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="watch-vid-title">'
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])/".watch-video-desc").to_html.ircify_html
45 # suboptimal, but still better than the default HTML info extractor
46 dm = /<div\s+class="watch-video-desc"[^>]*>/.match(s[:text])
47 content ||= dm ? dm.post_match.ircify_html : '(no description found)'
48 return {:title => s[:text].ircify_html_title, :content => content}
51 def youtube_apivideo_filter(s)
52 # This filter can be used either
53 e = s[:rexml] || REXML::Document.new(s[:text]).elements["entry"]
54 # TODO precomputing mg doesn't work on my REXML, despite what the doc
56 # mg = e.elements["media:group"]
57 # :title => mg["media:title"].text
58 # fails because "media:title" is not an Integer. Bah
61 :author => (e.elements["author/name"].text rescue nil),
62 :title => (e.elements["media:group/media:title"].text rescue nil),
63 :desc => (e.elements["media:group/media:description"].text rescue nil),
64 :cat => (e.elements["media:group/media:category"].text rescue nil),
65 :seconds => (e.elements["media:group/yt:duration/@seconds"].value.to_i rescue nil),
66 :url => (e.elements["media:group/media:player/@url"].value rescue nil),
67 :rating => (("%s/%s" % [e.elements["gd:rating/@average"].value, e.elements["gd:rating/@max"].value]) rescue nil),
68 :views => (e.elements["yt:statistics/@viewCount"].value rescue nil),
69 :faves => (e.elements["yt:statistics/@favoriteCount"].value rescue nil)
72 vid[:desc].gsub!(/\s+/m, " ")
74 if secs = vid[:seconds]
75 vid[:duration] = Utils.secs_to_short(secs)
77 vid[:duration] = _("unknown duration")
79 e.elements.each("media:group/media:content") { |c|
80 if url = (c.elements["@url"].value rescue nil)
81 type = c.elements["@type"].value rescue nil
82 medium = c.elements["@medium"].value rescue nil
83 expression = c.elements["@expression"].value rescue nil
84 seconds = c.elements["@duration"].value.to_i rescue nil
85 fmt = case num_fmt = (c.elements["@yt:format"].value rescue nil)
98 :url => url, :type => type,
99 :medium => medium, :expression => expression,
101 :numeric_format => num_fmt,
103 }.delete_if { |k, v| v.nil? }
105 vid[:formats].last[:duration] = Utils.secs_to_short(seconds)
107 vid[:formats].last[:duration] = _("unknown duration")
115 def youtube_apisearch_filter(s)
119 doc = REXML::Document.new(s[:text])
120 title = doc.elements["feed/title"].text
121 doc.elements.each("*/entry") { |e|
122 vids << @bot.filter(:"youtube.apivideo", :rexml => e)
128 return {:title => title, :vids => vids}
131 def youtube_search_filter(s)
133 # hits = s[:hits] || @bot.config['youtube.hits']
138 # Filter a YouTube video URL
139 def youtube_video_filter(s)
140 id = s[:youtube_video_id]
142 url = s.key?(:headers) ? s[:headers]['x-rbot-location'].first : s[:url]
144 id = YOUTUBE_VIDEO_URLS.match(url).captures.first rescue nil
150 url = YOUTUBE_VIDEO % {:id => id}
151 resp, xml = @bot.httputil.get_response(url)
152 unless Net::HTTPSuccess === resp
153 debug("error looking for movie %{id} on youtube: %{e}" % {:id => id, :e => xml})
158 return @bot.filter(:"youtube.apivideo", DataStream.new(xml, s))
167 @bot.register_filter(:youtube, :htmlinfo) { |s| youtube_filter(s) }
168 @bot.register_filter(:apisearch, :youtube) { |s| youtube_apisearch_filter(s) }
169 @bot.register_filter(:apivideo, :youtube) { |s| youtube_apivideo_filter(s) }
170 @bot.register_filter(:search, :youtube) { |s| youtube_search_filter(s) }
171 @bot.register_filter(:video, :youtube) { |s| youtube_video_filter(s) }
175 movie = params[:movie]
177 if movie =~ /^[A-Za-z0-9]+$/
181 vid = @bot.filter(:"youtube.video", :url => movie, :youtube_video_id => id)
183 str = _("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") %
184 {:bold => Bold}.merge(vid)
185 if @bot.config['youtube.formats'] and not vid[:formats].empty?
186 str << _("\n -- also available at: ")
187 str << vid[:formats].inject([]) { |list, fmt|
188 list << ("%{url} %{type} %{format} (%{duration} %{expression} %{medium})" % fmt)
193 m.reply(_("couldn't retrieve video info") % {:id => id})
197 def search(m, params)
198 what = params[:words].to_s
199 searchfor = CGI.escape what
200 url = YOUTUBE_SEARCH % {:words => searchfor}
201 resp, xml = @bot.httputil.get_response(url)
202 unless Net::HTTPSuccess === resp
203 m.reply(_("error looking for %{what} on youtube: %{e}") % {:what => what, :e => xml})
206 debug "filtering XML"
207 vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
211 m.reply _("no videos found for %{what}") % {:what => what}
214 show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
215 m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
218 shorts = vids.inject([]) { |list, el|
220 list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
222 m.reply(_("Videos for %{what}: %{shorts}") % {:what =>what, :shorts => shorts},
223 :split_at => /\s+\|\s+/)
224 if (descs = @bot.config['youtube.descs']) > 0
225 vids[0, descs].each_with_index { |v, i|
226 m.reply("[#{i+1}] %{title} (%{duration}): %{desc}" % v, :overlong => :truncate)
234 plugin = YouTubePlugin.new
236 plugin.map "youtube info :movie", :action => 'info', :threaded => true
237 plugin.map "youtube [search] *words", :action => 'search', :threaded => true