viddl-rb 0.7 → 0.8

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