viddl-rb 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,7 +10,36 @@ module ViddlRb
10
10
  ViddlRb.class_eval(File.read(plugin))
11
11
  end
12
12
  end
13
- end
14
13
 
15
- end
14
+ #checks to see whether the os has a certain utility like wget or curl
15
+ #`` returns the standard output of the process
16
+ #system returns the exit code of the process
17
+ def self.os_has?(utility)
18
+ windows = ENV['OS'] =~ /windows/i
19
+
20
+ unless windows
21
+ `which #{utility}`.include?(utility.to_s)
22
+ else
23
+ if !system("where /q where").nil? #if Windows has the where utility
24
+ system("where /q #{utility}") #/q is the quiet mode flag
25
+ else
26
+ begin #as a fallback we just run the utility itself
27
+ system(utility)
28
+ rescue Errno::ENOENT
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
16
34
 
35
+ #recursively get the final location (after following all redirects) for an url.
36
+ def self.get_final_location(url)
37
+ Net::HTTP.get_response(URI(url)) do |res|
38
+ location = res["location"]
39
+ return url if location.nil?
40
+ return get_final_location(location)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
data/lib/viddl-rb.rb CHANGED
@@ -95,19 +95,9 @@ module ViddlRb
95
95
  def self.follow_all_redirects(urls_filenames)
96
96
  urls_filenames.map do |uf|
97
97
  url = uf[:url]
98
- final_location = get_final_location(url)
98
+ final_location = UtilityHelper.get_final_location(url)
99
99
  {:url => final_location, :name => uf[:name]}
100
100
  end
101
101
  end
102
102
  private_class_method :follow_all_redirects
103
-
104
- #recursively get the final location (after following all redirects) for an url.
105
- def self.get_final_location(url)
106
- Net::HTTP.get_response(URI(url)) do |res|
107
- location = res["location"]
108
- return url if location.nil?
109
- return get_final_location(location)
110
- end
111
- end
112
- private_class_method :get_final_location
113
103
  end
@@ -1,4 +1,4 @@
1
- require 'rest_client'
1
+ require 'open-uri'
2
2
  class Soundcloud < PluginBase
3
3
  # this will be called by the main app to check whether this plugin is responsible for the url passed
4
4
  def self.matches_provider?(url)
@@ -7,7 +7,7 @@ class Soundcloud < PluginBase
7
7
 
8
8
  # return the url for original video file and title
9
9
  def self.get_urls_and_filenames(url, options = {})
10
- doc = Nokogiri::HTML(RestClient.get(url).body)
10
+ doc = Nokogiri::HTML(open(get_http_url(url)))
11
11
  download_filename = doc.at("#main-content-inner img[class=waveform]").attributes["src"].value.to_s.match(/\.com\/(.+)\_/)[1]
12
12
  download_url = "http://media.soundcloud.com/stream/#{download_filename}"
13
13
  file_name = transliterate("#{doc.at('//h1/em').text.chomp}") + ".mp3"
@@ -16,24 +16,27 @@ class Soundcloud < PluginBase
16
16
  end
17
17
 
18
18
  def self.transliterate(str)
19
- # Based on permalink_fu by Rick Olsen
19
+ # Based on permalink_fu by Rick Olsen
20
20
 
21
- # Downcase string
22
- str.downcase!
21
+ # Downcase string
22
+ str.downcase!
23
23
 
24
- # Remove apostrophes so isn't changes to isnt
25
- str.gsub!(/'/, '')
24
+ # Remove apostrophes so isn't changes to isnt
25
+ str.gsub!(/'/, '')
26
26
 
27
- # Replace any non-letter or non-number character with a space
28
- str.gsub!(/[^A-Za-z0-9]+/, ' ')
27
+ # Replace any non-letter or non-number character with a space
28
+ str.gsub!(/[^A-Za-z0-9]+/, ' ')
29
29
 
30
- # Remove spaces from beginning and end of string
31
- str.strip!
30
+ # Remove spaces from beginning and end of string
31
+ str.strip!
32
32
 
33
- # Replace groups of spaces with single hyphen
34
- str.gsub!(/\ +/, '-')
33
+ # Replace groups of spaces with single hyphen
34
+ str.gsub!(/\ +/, '-')
35
35
 
36
- str
37
- end
36
+ str
37
+ end
38
38
 
39
+ def self.get_http_url(url)
40
+ url.sub(/https?:\/\//, "http:\/\/")
41
+ end
39
42
  end
data/plugins/youtube.rb CHANGED
@@ -1,196 +1,271 @@
1
-
2
- class Youtube < PluginBase
3
- #this will be called by the main app to check whether this plugin is responsible for the url passed
4
- def self.matches_provider?(url)
5
- url.include?("youtube.com") || url.include?("youtu.be")
6
- end
7
-
8
- #get all videos and return their urls in an array
9
- def self.get_video_urls(feed_url)
10
- puts "[YOUTUBE] Retrieving videos..."
11
- urls_titles = Hash.new
12
- result_feed = Nokogiri::XML(open(feed_url))
13
- urls_titles.merge!(grab_ut(result_feed))
14
-
15
- #as long as the feed has a next link we follow it and add the resulting video urls
16
- loop do
17
- next_link = result_feed.search("//feed/link[@rel='next']").first
18
- break if next_link.nil?
19
- result_feed = Nokogiri::HTML(open(next_link["href"]))
20
- urls_titles.merge!(grab_ut(result_feed))
21
- end
22
-
23
- self.filter_urls(urls_titles)
24
- end
25
-
26
- #returns only the urls that match the --filter argument regex (if present)
27
- def self.filter_urls(url_hash)
28
- if @filter
29
- puts "[YOUTUBE] Using filter: #{@filter}"
30
- filtered = url_hash.select { |url, title| title =~ @filter }
31
- filtered.keys
32
- else
33
- url_hash.keys
34
- end
35
- end
36
-
37
- #extract all video urls and their titles from a feed and return in a hash
38
- def self.grab_ut(feed)
39
- feed.remove_namespaces! #so that we can get to the titles easily
40
- urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
41
- titles = feed.search("//entry/group/title").map { |title| title.text }
42
- Hash[urls.zip(titles)] #hash like this: url => title
43
- end
44
-
45
- def self.parse_playlist(url)
46
- #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
47
- #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
48
- #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
49
-
50
- playlist_ID = url[/(?:list=PL|p=)(\w{16})&?/,1]
51
- puts "[YOUTUBE] Playlist ID: #{playlist_ID}"
52
- feed_url = "http://gdata.youtube.com/feeds/api/playlists/#{playlist_ID}?&max-results=50&v=2"
53
- url_array = self.get_video_urls(feed_url)
54
- puts "[YOUTUBE] #{url_array.size} links found!"
55
- url_array
56
- end
57
-
58
- def self.parse_user(username)
59
- puts "[YOUTUBE] User: #{username}"
60
- feed_url = "http://gdata.youtube.com/feeds/api/users/#{username}/uploads?&max-results=50&v=2"
61
- url_array = get_video_urls(feed_url)
62
- puts "[YOUTUBE] #{url_array.size} links found!"
63
- url_array
64
- end
65
-
66
- def self.get_urls_and_filenames(url, options = {})
67
- @filter = options[:playlist_filter] #used to filter a playlist in self.filter_urls
68
- return_values = []
69
- if url.include?("view_play_list") || url.include?("playlist?list=") #if playlist
70
- puts "[YOUTUBE] playlist found! analyzing..."
71
- files = self.parse_playlist(url)
72
- puts "[YOUTUBE] Starting playlist download"
73
- files.each do |file|
74
- puts "[YOUTUBE] Downloading next movie on the playlist (#{file})"
75
- return_values << self.grab_single_url_filename(file)
76
- end
77
- elsif match = url.match(/\/user\/([\w\d]+)$/) #if user url, e.g. youtube.com/user/woot
78
- username = match[1]
79
- video_urls = self.parse_user(username)
80
- puts "[YOUTUBE] Starting user videos download"
81
- video_urls.each do |url|
82
- puts "[YOUTUBE] Downloading next user video (#{url})"
83
- return_values << self.grab_single_url_filename(url)
84
- end
85
- else #if single video
86
- return_values << self.grab_single_url_filename(url)
87
- end
88
- return_values.reject! { |value| value == :no_embed } #remove results that can not be downloaded
89
-
90
- if return_values.empty?
91
- raise CouldNotDownloadVideoError, "No videos could be downloaded - embedding disabled."
92
- else
93
- return_values
94
- end
95
- end
96
-
97
- def self.grab_single_url_filename(url)
98
- #the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
99
- #addition: might also look like this /v/abc5-a5afe5agae6g
100
- # alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
101
- # First get the redirect
102
- if url.include?("youtu.be")
103
- url = open(url).base_uri.to_s
104
- end
105
- video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/,2]
106
- if video_id.nil?
107
- raise CouldNotDownloadVideoError, "No video id found."
108
- else
109
- puts "[YOUTUBE] ID FOUND: #{video_id}"
110
- end
111
- #let's get some infos about the video. data is urlencoded
112
- yt_url = "http://www.youtube.com/get_video_info?video_id=#{video_id}"
113
- video_info = RestClient.get(yt_url).body
114
- #converting the huge infostring into a hash. simply by splitting it at the & and then splitting it into key and value arround the =
115
- #[...]blabla=blubb&narf=poit&marc=awesome[...]
116
- video_info_hash = Hash[*video_info.split("&").collect { |v|
117
- key, encoded_value = v.split("=")
118
- if encoded_value.to_s.empty?
119
- value = ""
120
- else
121
- #decode until everything is "normal"
122
- while (encoded_value != CGI::unescape(encoded_value)) do
123
- #"decoding"
124
- encoded_value = CGI::unescape(encoded_value)
125
- end
126
- value = encoded_value
127
- end
128
-
129
- if key =~ /_map/
130
- orig_value = value
131
- value = value.split(",")
132
- if key == "url_encoded_fmt_stream_map"
133
- url_array = orig_value.split("url=").map{|url_string| url_string.chomp(",")}
134
- result_hash = {}
135
- url_array.each do |url|
136
- next if url.to_s.empty? || url.to_s.match(/^itag/)
137
- format_id = url[/\&itag=(\d+)/, 1]
138
- result_hash[format_id] = url
139
- end
140
- value = result_hash
141
- elsif key == "fmt_map"
142
- value = Hash[*value.collect { |v|
143
- k2, *v2 = v.split("/")
144
- [k2, v2]
145
- }.flatten(1)]
146
- elsif key == "fmt_url_map" || key == "fmt_stream_map"
147
- Hash[*value.collect { |v| v.split("|")}.flatten]
148
- end
149
- end
150
- [key, value]
151
- }.flatten]
152
-
153
- if video_info_hash["status"] == "fail"
154
- return :no_embed
155
- end
156
-
157
- title = video_info_hash["title"]
158
- length_s = video_info_hash["length_seconds"]
159
- token = video_info_hash["token"]
160
-
161
- #for the formats, see: http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
162
- fmt_list = video_info_hash["fmt_list"].split(",")
163
- available_formats = fmt_list.map{|format| format.split("/").first}
164
-
165
- format_ext = {}
166
- format_ext["38"] = {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"}
167
- format_ext["37"] = {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"}
168
- format_ext["22"] = {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"}
169
- format_ext["45"] = {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"}
170
- format_ext["44"] = {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"}
171
- format_ext["18"] = {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"}
172
- format_ext["35"] = {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"}
173
- format_ext["34"] = {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"}
174
- format_ext["5"] = {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"}
175
- format_ext["17"] = {:extension => "3gp", :name => "3gp"}
176
-
177
- #since 1.8 doesn't do ordered hashes
178
- prefered_order = ["38","37","22","45","44","18","35","34","5","17"]
179
-
180
- selected_format = prefered_order.select{|possible_format| available_formats.include?(possible_format)}.first
181
-
182
- puts "[YOUTUBE] Title: #{title}"
183
- puts "[YOUTUBE] Length: #{length_s} s"
184
- puts "[YOUTUBE] t-parameter: #{token}"
185
- #best quality seems always to be firsts
186
- puts "[YOUTUBE] formats available: #{available_formats.inspect} (downloading format #{selected_format} -> #{format_ext[selected_format][:name]})"
187
-
188
- #video_info_hash.keys.sort.each{|key| puts "#{key} : #{video_info_hash[key]}" }
189
- download_url = video_info_hash["url_encoded_fmt_stream_map"][selected_format]
190
- #if download url ends with a ';' followed by a codec string remove that part because it stops URI.parse from working
191
- download_url = $1 if download_url =~ /(.*?);\scodecs=/
192
- file_name = PluginBase.make_filename_safe(title) + "." + format_ext[selected_format][:extension]
193
- puts "downloading to " + file_name
194
- {:url => download_url, :name => file_name}
195
- end
196
- 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 46 45 44 43 18 35 34 5 17]
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
+ grab_url_embeddable(url) || grab_url_non_embeddable(url)
57
+ end
58
+
59
+ def self.grab_url_embeddable(url)
60
+ video_info = get_video_info(url)
61
+ video_params = extract_video_parameters(video_info)
62
+ unless video_params[:embeddable]
63
+ notify("VIDEO IS NOT EMBEDDABLE")
64
+ return false
65
+ end
66
+
67
+ urls_formats = extract_urls_formats(video_info)
68
+ selected_format = choose_format(urls_formats)
69
+ title = video_params[:title]
70
+ file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
71
+
72
+ {:url => urls_formats[selected_format], :name => file_name}
73
+ end
74
+
75
+ def self.grab_url_non_embeddable(url)
76
+ video_info = open(url).read
77
+ stream_map = video_info[/url_encoded_fmt_stream_map\" *: *\"([^\"]+)\"/,1]
78
+ urls_formats = parse_stream_map(url_decode(stream_map))
79
+ selected_format = choose_format(urls_formats)
80
+ title = video_info[/<meta name="title" content="([^"]*)">/, 1]
81
+ file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
82
+
83
+ # cleaning
84
+ clean_url = urls_formats[selected_format].gsub(/\\u0026[^&]*/, "")
85
+
86
+ {:url => clean_url, :name => file_name}
87
+ end
88
+
89
+ def self.get_video_info(url)
90
+ id = extract_video_id(url)
91
+ request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
92
+ open(request_url).read
93
+ end
94
+
95
+ def self.extract_video_id(url)
96
+ # the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
97
+ # addition: might also look like this /v/abc5-a5afe5agae6g
98
+ # alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
99
+ url = open(url).base_uri.to_s if url.include?("youtu.be")
100
+ video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
101
+
102
+ if video_id
103
+ notify("ID FOUND: #{video_id}")
104
+ video_id
105
+ else
106
+ download_error("No video id found.")
107
+ end
108
+ end
109
+
110
+ def self.extract_video_parameters(video_info)
111
+ decoded = url_decode(video_info)
112
+
113
+ {:title => decoded[/title=(.+?)(?:&|$)/, 1],
114
+ :length_sec => decoded[/length_seconds=(.+?)(?:&|$)/, 1],
115
+ :author => decoded[/author=(.+?)(?:&|$)/, 1],
116
+ :embeddable => !decoded.include?("status=fail")}
117
+ end
118
+
119
+ def self.extract_urls_formats(video_info)
120
+ stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
121
+ parse_stream_map(stream_map)
122
+ end
123
+
124
+ def self.parse_stream_map(stream_map)
125
+ urls = extract_download_urls(stream_map)
126
+ formats_urls = {}
127
+
128
+ urls.each do |url|
129
+ format = url[/itag=(\d+)/, 1]
130
+ formats_urls[format] = url
131
+ end
132
+
133
+ formats_urls
134
+ end
135
+
136
+ def self.extract_download_urls(stream_map)
137
+ entries = stream_map.split("%2C")
138
+ decoded = entries.map { |entry| url_decode(entry) }
139
+
140
+ decoded.map do |entry|
141
+ url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
142
+ sig = entry[/sig=(.+?)(?:&|$)/, 1]
143
+
144
+ url + "&signature=#{sig}"
145
+ end
146
+ end
147
+
148
+ def self.choose_format(urls_formats)
149
+ available_formats = urls_formats.keys
150
+
151
+ if @quality #if the user specified a format
152
+ ext = @quality[:extension]
153
+ res = @quality[:resolution]
154
+ #gets a nested array with all the formats of the same res as the user wanted
155
+ requested = VIDEO_FORMATS.select { |id, format| format[:name].include?(res) }.to_a
156
+
157
+ if requested.empty?
158
+ notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
159
+ get_default_format(available_formats)
160
+ else
161
+ pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
162
+ pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
163
+ end
164
+ else
165
+ get_default_format(available_formats)
166
+ end
167
+ end
168
+
169
+ def self.get_default_format(available)
170
+ DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
171
+ end
172
+
173
+ def self.url_decode(text)
174
+ while text != (decoded = CGI::unescape(text)) do
175
+ text = decoded
176
+ end
177
+ text
178
+ end
179
+
180
+ def self.notify(message)
181
+ puts "[YOUTUBE] #{message}"
182
+ end
183
+
184
+ def self.download_error(message)
185
+ raise CouldNotDownloadVideoError, message
186
+ end
187
+
188
+ #
189
+ # class PlaylistParser
190
+ #_____________________
191
+
192
+ class PlaylistParser
193
+
194
+ PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
195
+ USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
196
+
197
+ def get_playlist_urls(url, filter = nil)
198
+ @filter = filter
199
+
200
+ if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
201
+ parse_playlist(url)
202
+ elsif username = url[/\/user\/([\w\d]+)(?:\/|$)/, 1] # if user URL
203
+ parse_user(username)
204
+ else # if neither return nil
205
+ nil
206
+ end
207
+ end
208
+
209
+ def parse_playlist(url)
210
+ #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
211
+ #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
212
+ #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
213
+
214
+ playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
215
+ notify "Playlist ID: #{playlist_ID}"
216
+ feed_url = PLAYLIST_FEED % playlist_ID
217
+ url_array = get_video_urls(feed_url)
218
+ notify "#{url_array.size} links found!"
219
+ url_array
220
+ end
221
+
222
+ def parse_user(username)
223
+ notify "User: #{username}"
224
+ feed_url = USER_FEED % username
225
+ url_array = get_video_urls(feed_url)
226
+ notify "#{url_array.size} links found!"
227
+ url_array
228
+ end
229
+
230
+ #get all videos and return their urls in an array
231
+ def get_video_urls(feed_url)
232
+ notify "Retrieving videos..."
233
+ urls_titles = {}
234
+ result_feed = Nokogiri::XML(open(feed_url))
235
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
236
+
237
+ #as long as the feed has a next link we follow it and add the resulting video urls
238
+ loop do
239
+ next_link = result_feed.search("//feed/link[@rel='next']").first
240
+ break if next_link.nil?
241
+ result_feed = Nokogiri::HTML(open(next_link["href"]))
242
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
243
+ end
244
+
245
+ filter_urls(urls_titles)
246
+ end
247
+
248
+ #extract all video urls and their titles from a feed and return in a hash
249
+ def grab_urls_and_titles(feed)
250
+ feed.remove_namespaces! #so that we can get to the titles easily
251
+ urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
252
+ titles = feed.search("//entry/group/title").map { |title| title.text }
253
+ Hash[urls.zip(titles)] #hash like this: url => title
254
+ end
255
+
256
+ #returns only the urls that match the --filter argument regex (if present)
257
+ def filter_urls(url_hash)
258
+ if @filter
259
+ notify "Using filter: #{@filter}"
260
+ filtered = url_hash.select { |url, title| title =~ @filter }
261
+ filtered.keys
262
+ else
263
+ url_hash.keys
264
+ end
265
+ end
266
+
267
+ def notify(message)
268
+ Youtube.notify(message)
269
+ end
270
+ end
271
+ end