4 # :title: YouTube plugin for rbot
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 # Copyright:: (C) 2008 Giuseppe Bilotta
11 class YouTubePlugin < Plugin
12 YOUTUBE_SEARCH = "http://gdata.youtube.com/feeds/api/videos?vq=%{words}&orderby=relevance"
13 YOUTUBE_VIDEO = "http://gdata.youtube.com/feeds/api/videos/%{id}"
15 YOUTUBE_VIDEO_URLS = %r{youtube.com/(?:watch\?v=|v/)(.*?)(&.*)?$}
17 Config.register Config::IntegerValue.new('youtube.hits',
19 :desc => "Number of hits to return from YouTube searches")
20 Config.register Config::IntegerValue.new('youtube.descs',
22 :desc => "When set to n > 0, the bot will return the description of the first n videos found")
25 loc = Utils.check_location(s, /youtube\.com/)
27 if s[:text].include? '<div id="vidTitle">'
28 vid = @bot.filter(:"youtube.video", s)
30 content = _("Category: %{cat}. Rating: %{rating}. Author: %{author}. Duration: %{duration}. %{views} views, faved %{faves} times. %{desc}") % vid
31 return vid.merge(:content => content)
32 elsif s[:text].include? '<!-- start search results -->'
33 vids = @bot.filter(:"youtube.search", s)[:videos]
38 # otherwise, just grab the proper div
40 content = (Hpricot(s[:text])/"#mainContent").to_html.ircify_html
42 # suboptimal, but still better than the default HTML info extractor
43 content ||= /<div id="mainContent"[^>]*>/.match(s[:text]).post_match.ircify_html
44 return {:title => s[:text].ircify_html_title, :content => content}
47 def youtube_apivideo_filter(s)
48 # This filter can be used either
49 e = s[:rexml] || REXML::Document.new(s[:text]).elements["entry"]
50 # TODO precomputing mg doesn't work on my REXML, despite what the doc
52 # mg = e.elements["media:group"]
53 # :title => mg["media:title"].text
54 # fails because "media:title" is not an Integer. Bah
57 :author => (e.elements["author/name"].text rescue nil),
58 :title => (e.elements["media:group/media:title"].text rescue nil),
59 :desc => (e.elements["media:group/media:description"].text rescue nil),
60 :cat => (e.elements["media:group/media:category"].text rescue nil),
61 :seconds => (e.elements["media:group/yt:duration/@seconds"].value.to_i rescue nil),
62 :url => (e.elements["media:group/media:player/@url"].value rescue nil),
63 :rating => (("%s/%s" % [e.elements["gd:rating/@average"].value, e.elements["gd:rating/@max"].value]) rescue nil),
64 :views => (e.elements["yt:statistics/@viewCount"].value rescue nil),
65 :faves => (e.elements["yt:statistics/@favoriteCount"].value rescue nil)
68 vid[:desc].gsub!(/\s+/m, " ")
70 if secs = vid[:seconds]
71 mins, secs = secs.divmod 60
72 hours, mins = mins.divmod 60
74 vid[:duration] = "%s:%s:%s" % [hours, mins, secs]
76 vid[:duration] = "%s'%s\"" % [mins, secs]
78 vid[:duration] = "%ss" % [secs]
81 vid[:duration] = _("unknown duration")
83 e.elements.each("media:group/media:content") { |c|
84 if url = (c.elements["@url"].value rescue nil)
85 type = c.elements["@type"].value rescue nil
86 medium = c.elements["@medium"].value rescue nil
87 expression = c.elements["@expression"].value rescue nil
88 duration = c.elements["@duration"].value rescue nil
89 fmt = case num_fmt = (c.elements["@yt:format"].value rescue nil)
102 :url => url, :type => type,
103 :medium => medium, :expression => expression,
104 :duration => duration,
105 :numeric_format => num_fmt,
107 }.delete_if { |k, v| v.nil? }
114 def youtube_apisearch_filter(s)
118 doc = REXML::Document.new(s[:text])
119 title = doc.elements["feed/title"].text
120 doc.elements.each("*/entry") { |e|
121 vids << @bot.filter(:"youtube.apivideo", :rexml => e)
127 return {:title => title, :vids => vids}
130 def youtube_search_filter(s)
132 # hits = s[:hits] || @bot.config['youtube.hits']
137 # Filter a YouTube video URL
138 def youtube_video_filter(s)
139 id = s[:youtube_video_id]
141 url = s.key?(:headers) ? s[:headers]['x-rbot-location'].first : s[:url]
143 id = YOUTUBE_VIDEO_URLS.match(url).captures.first rescue nil
149 url = YOUTUBE_VIDEO % {:id => id}
150 resp, xml = @bot.httputil.get_response(url)
151 unless Net::HTTPSuccess === resp
152 debug("error looking for movie %{id} on youtube: %{e}" % {:id => id, :e => xml})
157 return @bot.filter(:"youtube.apivideo", DataStream.new(xml, s))
166 @bot.register_filter(:youtube, :htmlinfo) { |s| youtube_filter(s) }
167 @bot.register_filter(:apisearch, :youtube) { |s| youtube_apisearch_filter(s) }
168 @bot.register_filter(:apivideo, :youtube) { |s| youtube_apivideo_filter(s) }
169 @bot.register_filter(:search, :youtube) { |s| youtube_search_filter(s) }
170 @bot.register_filter(:video, :youtube) { |s| youtube_video_filter(s) }
174 movie = params[:movie]
176 if movie =~ /^[A-Za-z0-9]+$/
180 vid = @bot.filter(:"youtube.video", :url => movie, :youtube_video_id => id)
182 m.reply(_("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") %
183 {:bold => Bold}.merge(vid))
185 m.reply(_("couldn't retrieve video info") % {:id => id})
189 def search(m, params)
190 what = params[:words].to_s
191 searchfor = CGI.escape what
192 url = YOUTUBE_SEARCH % {:words => searchfor}
193 resp, xml = @bot.httputil.get_response(url)
194 unless Net::HTTPSuccess === resp
195 m.reply(_("error looking for %{what} on youtube: %{e}") % {:what => what, :e => xml})
198 debug "filtering XML"
199 vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
203 m.reply _("no videos found for %{what}") % {:what => what}
206 show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
207 m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
210 shorts = vids.inject([]) { |list, el|
212 list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
214 m.reply(_("Videos for %{what}: %{shorts}") % {:what =>what, :shorts => shorts},
215 :split_at => /\s+\|\s+/)
216 if (descs = @bot.config['youtube.descs']) > 0
217 vids[0, descs].each_with_index { |v, i|
218 m.reply("[#{i+1}] %{title} (%{duration}): %{desc}" % v, :overlong => :truncate)
226 plugin = YouTubePlugin.new
228 plugin.map "youtube info :movie", :action => 'info', :threaded => true
229 plugin.map "youtube [search] *words", :action => 'search', :threaded => true