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")
23 Config.register Config::BooleanValue.new('youtube.formats',
25 :desc => "Should the bot display alternative URLs (swf, rstp) for YouTube videos?")
28 loc = Utils.check_location(s, /youtube\.com/)
30 if s[:text].include? '<div id="vidTitle">'
31 vid = @bot.filter(:"youtube.video", s)
33 content = _("Category: %{cat}. Rating: %{rating}. Author: %{author}. Duration: %{duration}. %{views} views, faved %{faves} times. %{desc}") % vid
34 return vid.merge(:content => content)
35 elsif s[:text].include? '<!-- start search results -->'
36 vids = @bot.filter(:"youtube.search", s)[:videos]
41 # otherwise, just grab the proper div
43 content = (Hpricot(s[:text])/"#mainContent").to_html.ircify_html
45 # suboptimal, but still better than the default HTML info extractor
46 content ||= /<div id="mainContent"[^>]*>/.match(s[:text]).post_match.ircify_html
47 return {:title => s[:text].ircify_html_title, :content => content}
50 def youtube_apivideo_filter(s)
51 # This filter can be used either
52 e = s[:rexml] || REXML::Document.new(s[:text]).elements["entry"]
53 # TODO precomputing mg doesn't work on my REXML, despite what the doc
55 # mg = e.elements["media:group"]
56 # :title => mg["media:title"].text
57 # fails because "media:title" is not an Integer. Bah
60 :author => (e.elements["author/name"].text rescue nil),
61 :title => (e.elements["media:group/media:title"].text rescue nil),
62 :desc => (e.elements["media:group/media:description"].text rescue nil),
63 :cat => (e.elements["media:group/media:category"].text rescue nil),
64 :seconds => (e.elements["media:group/yt:duration/@seconds"].value.to_i rescue nil),
65 :url => (e.elements["media:group/media:player/@url"].value rescue nil),
66 :rating => (("%s/%s" % [e.elements["gd:rating/@average"].value, e.elements["gd:rating/@max"].value]) rescue nil),
67 :views => (e.elements["yt:statistics/@viewCount"].value rescue nil),
68 :faves => (e.elements["yt:statistics/@favoriteCount"].value rescue nil)
71 vid[:desc].gsub!(/\s+/m, " ")
73 if secs = vid[:seconds]
74 vid[:duration] = Utils.secs_to_short(secs)
76 vid[:duration] = _("unknown duration")
78 e.elements.each("media:group/media:content") { |c|
79 if url = (c.elements["@url"].value rescue nil)
80 type = c.elements["@type"].value rescue nil
81 medium = c.elements["@medium"].value rescue nil
82 expression = c.elements["@expression"].value rescue nil
83 seconds = c.elements["@duration"].value.to_i rescue nil
84 fmt = case num_fmt = (c.elements["@yt:format"].value rescue nil)
97 :url => url, :type => type,
98 :medium => medium, :expression => expression,
100 :numeric_format => num_fmt,
102 }.delete_if { |k, v| v.nil? }
104 vid[:formats].last[:duration] = Utils.secs_to_short(seconds)
106 vid[:formats].last[:duration] = _("unknown duration")
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 str = _("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") %
183 {:bold => Bold}.merge(vid)
184 if @bot.config['youtube.formats'] and not vid[:formats].empty?
185 str << _("\n -- also available at: ")
186 str << vid[:formats].inject([]) { |list, fmt|
187 list << ("%{url} %{type} %{format} (%{duration} %{expression} %{medium})" % fmt)
192 m.reply(_("couldn't retrieve video info") % {:id => id})
196 def search(m, params)
197 what = params[:words].to_s
198 searchfor = CGI.escape what
199 url = YOUTUBE_SEARCH % {:words => searchfor}
200 resp, xml = @bot.httputil.get_response(url)
201 unless Net::HTTPSuccess === resp
202 m.reply(_("error looking for %{what} on youtube: %{e}") % {:what => what, :e => xml})
205 debug "filtering XML"
206 vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
210 m.reply _("no videos found for %{what}") % {:what => what}
213 show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
214 m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
217 shorts = vids.inject([]) { |list, el|
219 list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
221 m.reply(_("Videos for %{what}: %{shorts}") % {:what =>what, :shorts => shorts},
222 :split_at => /\s+\|\s+/)
223 if (descs = @bot.config['youtube.descs']) > 0
224 vids[0, descs].each_with_index { |v, i|
225 m.reply("[#{i+1}] %{title} (%{duration}): %{desc}" % v, :overlong => :truncate)
233 plugin = YouTubePlugin.new
235 plugin.map "youtube info :movie", :action => 'info', :threaded => true
236 plugin.map "youtube [search] *words", :action => 'search', :threaded => true