]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/youtube.rb
webhook: include target ref for pull/merge requests
[user/henk/code/ruby/rbot.git] / 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 help(plugin, topic="")
28     'youtube [search] <query> : searches youtube videos | youtube info <id> : returns description and video links'
29   end
30
31   def youtube_filter(s)
32     loc = Utils.check_location(s, /youtube\.com/)
33     return nil unless loc
34     if s[:text].include? '<link rel="alternate" type="text/xml+oembed"'
35       vid = @bot.filter(:"youtube.video", s)
36       return nil unless vid
37       content = _("Category: %{cat}. Rating: %{rating}. Author: %{author}. Duration: %{duration}. %{views} views, faved %{faves} times. %{desc}") % vid
38       return vid.merge(:content => content)
39     elsif s[:text].include? '<!-- start search results -->'
40       vids = @bot.filter(:"youtube.search", s)[:videos]
41       if !vids.empty?
42         return nil # TODO
43       end
44     end
45     # otherwise, just grab the proper div
46     if defined? Hpricot
47       content = (Hpricot(s[:text])/".watch-video-desc").to_html.ircify_html
48     end
49     # suboptimal, but still better than the default HTML info extractor
50     dm = /<div\s+class="watch-video-desc"[^>]*>/.match(s[:text])
51     content ||= dm ? dm.post_match.ircify_html : '(no description found)'
52     return {:title => s[:text].ircify_html_title, :content => content}
53   end
54
55   def youtube_apivideo_filter(s)
56     # This filter can be used either
57     e = s[:rexml] || REXML::Document.new(s[:text]).elements["entry"]
58     # TODO precomputing mg doesn't work on my REXML, despite what the doc
59     # says?
60     #   mg = e.elements["media:group"]
61     #   :title => mg["media:title"].text
62     # fails because "media:title" is not an Integer. Bah
63     vid = {
64       :formats => [],
65       :author => (e.elements["author/name"].text rescue nil),
66       :title =>  (e.elements["media:group/media:title"].text rescue nil),
67       :desc =>   (e.elements["media:group/media:description"].text rescue nil),
68       :cat => (e.elements["media:group/media:category"].text rescue nil),
69       :seconds => (e.elements["media:group/yt:duration/"].attributes["seconds"].to_i rescue nil),
70       :url => (e.elements["media:group/media:player/"].attributes["url"] rescue nil),
71       :rating => (("%s/%s" % [e.elements["gd:rating"].attributes["average"], e.elements["gd:rating/@max"].value]) rescue nil),
72       :views => (e.elements["yt:statistics"].attributes["viewCount"] rescue nil),
73       :faves => (e.elements["yt:statistics"].attributes["favoriteCount"] rescue nil)
74     }
75     if vid[:desc]
76       vid[:desc].gsub!(/\s+/m, " ")
77     end
78     if secs = vid[:seconds]
79       vid[:duration] = Utils.secs_to_short(secs)
80     else
81       vid[:duration] = _("unknown duration")
82     end
83     e.elements.each("media:group/media:content") { |c|
84       if url = (c.attributes["url"] rescue nil)
85         type = c.attributes["type"] rescue nil
86         medium = c.attributes["medium"] rescue nil
87         expression = c.attributes["expression"] rescue nil
88         seconds = c.attributes["duration"].to_i rescue nil
89         fmt = case num_fmt = (c.attributes["yt:format"] rescue nil)
90               when "1"
91                 "h263+amr"
92               when "5"
93                 "swf"
94               when "6"
95                 "mp4+aac"
96               when nil
97                 nil
98               else
99                 num_fmt
100               end
101         vid[:formats] << {
102           :url => url, :type => type,
103           :medium => medium, :expression => expression,
104           :seconds => seconds,
105           :numeric_format => num_fmt,
106           :format => fmt
107         }.delete_if { |k, v| v.nil? }
108         if seconds
109           vid[:formats].last[:duration] = Utils.secs_to_short(seconds)
110         else
111           vid[:formats].last[:duration] = _("unknown duration")
112         end
113       end
114     }
115     debug vid
116     return vid
117   end
118
119   def youtube_apisearch_filter(s)
120     vids = []
121     title = nil
122     begin
123 debug s.inspect
124       doc = REXML::Document.new(s[:text])
125       title = doc.elements["feed/title"].text
126       doc.elements.each("*/entry") { |e|
127         vids << @bot.filter(:"youtube.apivideo", :rexml => e)
128       }
129       debug vids
130     rescue => e
131       debug e
132     end
133     return {:title => title, :vids => vids}
134   end
135
136   def youtube_search_filter(s)
137     # TODO
138     # hits = s[:hits] || @bot.config['youtube.hits']
139     # scrap the videos
140     return []
141   end
142
143   # Filter a YouTube video URL
144   def youtube_video_filter(s)
145     id = s[:youtube_video_id]
146     if not id
147       url = s.key?(:headers) ? s[:headers]['x-rbot-location'].first : s[:url]
148       debug url
149       id = YOUTUBE_VIDEO_URLS.match(url).captures.first rescue nil
150     end
151     return nil unless id
152
153     debug id
154
155     url = YOUTUBE_VIDEO % {:id => id}
156     resp = @bot.httputil.get_response(url)
157     xml = resp.body
158     unless resp.kind_of? Net::HTTPSuccess
159       debug("error looking for movie %{id} on youtube: %{e}" % {:id => id, :e => xml})
160       return nil
161     end
162     debug xml
163     begin
164       return @bot.filter(:"youtube.apivideo", DataStream.new(xml, s))
165     rescue => e
166       debug e
167       return nil
168     end
169   end
170
171   def initialize
172     super
173     @bot.register_filter(:youtube, :htmlinfo) { |s| youtube_filter(s) }
174     @bot.register_filter(:apisearch, :youtube) { |s| youtube_apisearch_filter(s) }
175     @bot.register_filter(:apivideo, :youtube) { |s| youtube_apivideo_filter(s) }
176     @bot.register_filter(:search, :youtube) { |s| youtube_search_filter(s) }
177     @bot.register_filter(:video, :youtube) { |s| youtube_video_filter(s) }
178   end
179
180   def info(m, params)
181     movie = params[:movie]
182     id = nil
183     if movie =~ /^[A-Za-z0-9]+$/
184       id = movie.dup
185     end
186
187     vid = @bot.filter(:"youtube.video", :url => movie, :youtube_video_id => id)
188     if vid
189       str = _("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") %
190         {:bold => Bold}.merge(vid)
191       if @bot.config['youtube.formats'] and not vid[:formats].empty?
192         str << _("\n -- also available at: ")
193         str << vid[:formats].inject([]) { |list, fmt|
194           list << ("%{url} %{type} %{format} (%{duration} %{expression} %{medium})" % fmt)
195         }.join(', ')
196       end
197       m.reply str
198     else
199       m.reply(_("couldn't retrieve video info") % {:id => id})
200     end
201   end
202
203   def search(m, params)
204     what = params[:words].to_s
205     searchfor = CGI.escape what
206     url = YOUTUBE_SEARCH % {:words => searchfor}
207     resp = @bot.httputil.get_response(url)
208     xml = resp.body
209     unless resp.kind_of? Net::HTTPSuccess
210       m.reply(_("error looking for %{what} on youtube: %{e}") % {:what => what, :e => xml})
211       return
212     end
213     debug "filtering XML"
214     vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
215     debug vids
216     case vids.length
217     when 0
218       m.reply _("no videos found for %{what}") % {:what => what}
219       return
220     when 1
221       show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
222       m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
223     else
224       idx = 0
225       shorts = vids.inject([]) { |list, el|
226         idx += 1
227         list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
228       }.join(" | ")
229       m.reply(_("Videos for %{what}: %{shorts}") % {:what =>what, :shorts => shorts},
230               :split_at => /\s+\|\s+/)
231       if (descs = @bot.config['youtube.descs']) > 0
232         vids[0, descs].each_with_index { |v, i|
233           m.reply("[#{i+1}] %{title} (%{duration}): %{desc}" % v, :overlong => :truncate)
234         }
235       end
236     end
237   end
238
239 end
240
241 plugin = YouTubePlugin.new
242
243 plugin.map "youtube info :movie", :action => 'info', :threaded => true
244 plugin.map "youtube [search] *words", :action => 'search', :threaded => true
245