uno plugin: public announce of people on join
[rbot] / data / rbot / plugins / youtube.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: YouTube plugin for rbot
5 #
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
7 #
8 # Copyright:: (C) 2008 Giuseppe Bilotta
9
10
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}"
14
15   YOUTUBE_VIDEO_URLS = %r{youtube.com/(?:watch\?v=|v/)(.*?)(&.*)?$}
16
17   Config.register Config::IntegerValue.new('youtube.hits',
18     :default => 3,
19     :desc => "Number of hits to return from YouTube searches")
20   Config.register Config::IntegerValue.new('youtube.descs',
21     :default => 3,
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',
24     :default => true,
25     :desc => "Should the bot display alternative URLs (swf, rstp) for YouTube videos?")
26
27   def youtube_filter(s)
28     loc = Utils.check_location(s, /youtube\.com/)
29     return nil unless loc
30     if s[:text].include? '<div id="vidTitle">'
31       vid = @bot.filter(:"youtube.video", s)
32       return nil unless vid
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]
37       if !vids.empty?
38         return nil # TODO
39       end
40     end
41     # otherwise, just grab the proper div
42     if defined? Hpricot
43       content = (Hpricot(s[:text])/"#mainContent").to_html.ircify_html
44     end
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}
48   end
49
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
54     # says?
55     #   mg = e.elements["media:group"]
56     #   :title => mg["media:title"].text
57     # fails because "media:title" is not an Integer. Bah
58     vid = {
59       :formats => [],
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)
69     }
70     if vid[:desc]
71       vid[:desc].gsub!(/\s+/m, " ")
72     end
73     if secs = vid[:seconds]
74       vid[:duration] = Utils.secs_to_short(secs)
75     else
76       vid[:duration] = _("unknown duration")
77     end
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)
85               when "1"
86                 "h263+amr"
87               when "5"
88                 "swf"
89               when "6"
90                 "mp4+aac"
91               when nil
92                 nil
93               else
94                 num_fmt
95               end
96         vid[:formats] << {
97           :url => url, :type => type,
98           :medium => medium, :expression => expression,
99           :seconds => seconds,
100           :numeric_format => num_fmt,
101           :format => fmt
102         }.delete_if { |k, v| v.nil? }
103         if seconds
104           vid[:formats].last[:duration] = Utils.secs_to_short(seconds)
105         else
106           vid[:formats].last[:duration] = _("unknown duration")
107         end
108       end
109     }
110     debug vid
111     return vid
112   end
113
114   def youtube_apisearch_filter(s)
115     vids = []
116     title = nil
117     begin
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)
122       }
123       debug vids
124     rescue => e
125       debug e
126     end
127     return {:title => title, :vids => vids}
128   end
129
130   def youtube_search_filter(s)
131     # TODO
132     # hits = s[:hits] || @bot.config['youtube.hits']
133     # scrap the videos
134     return []
135   end
136
137   # Filter a YouTube video URL
138   def youtube_video_filter(s)
139     id = s[:youtube_video_id]
140     if not id
141       url = s.key?(:headers) ? s[:headers]['x-rbot-location'].first : s[:url]
142       debug url
143       id = YOUTUBE_VIDEO_URLS.match(url).captures.first rescue nil
144     end
145     return nil unless id
146
147     debug id
148
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})
153       return nil
154     end
155     debug xml
156     begin
157       return @bot.filter(:"youtube.apivideo", DataStream.new(xml, s))
158     rescue => e
159       debug e
160       return nil
161     end
162   end
163
164   def initialize
165     super
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) }
171   end
172
173   def info(m, params)
174     movie = params[:movie]
175     id = nil
176     if movie =~ /^[A-Za-z0-9]+$/
177       id = movie.dup
178     end
179
180     vid = @bot.filter(:"youtube.video", :url => movie, :youtube_video_id => id)
181     if vid
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)
188         }.join(', ')
189       end
190       m.reply str
191     else
192       m.reply(_("couldn't retrieve video info") % {:id => id})
193     end
194   end
195
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})
203       return
204     end
205     debug "filtering XML"
206     vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
207     debug vids
208     case vids.length
209     when 0
210       m.reply _("no videos found for %{what}") % {:what => what}
211       return
212     when 1
213       show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
214       m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
215     else
216       idx = 0
217       shorts = vids.inject([]) { |list, el|
218         idx += 1
219         list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
220       }.join(" | ")
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)
226         }
227       end
228     end
229   end
230
231 end
232
233 plugin = YouTubePlugin.new
234
235 plugin.map "youtube info :movie", :action => 'info', :threaded => true
236 plugin.map "youtube [search] *words", :action => 'search', :threaded => true