]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/youtube.rb
1faa7c35ed01b2e5a71cb13afd8bd4d7d2d693f7
[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
24   def youtube_filter(s)
25     loc = Utils.check_location(s, /youtube\.com/)
26     return nil unless loc
27     if s[:text].include? '<div id="vidTitle">'
28       vid = @bot.filter(:"youtube.video", s)
29       return nil unless vid
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]
34       if !vids.empty?
35         return nil # TODO
36       end
37     end
38     # otherwise, just grab the proper div
39     if defined? Hpricot
40       content = (Hpricot(s[:text])/"#mainContent").to_html.ircify_html
41     end
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}
45   end
46
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
51     # says?
52     #   mg = e.elements["media:group"]
53     #   :title => mg["media:title"].text
54     # fails because "media:title" is not an Integer. Bah
55     vid = {
56       :formats => [],
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)
66     }
67     if vid[:desc]
68       vid[:desc].gsub!(/\s+/m, " ")
69     end
70     if secs = vid[:seconds]
71       mins, secs = secs.divmod 60
72       hours, mins = mins.divmod 60
73       if hours > 0
74         vid[:duration] = "%s:%s:%s" % [hours, mins, secs]
75       elsif mins > 0
76         vid[:duration] = "%s'%s\"" % [mins, secs]
77       else
78         vid[:duration] = "%ss" % [secs]
79       end
80     else
81       vid[:duration] = _("unknown duration")
82     end
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)
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           :duration => duration,
105           :numeric_format => num_fmt,
106           :format => fmt
107         }.delete_if { |k, v| v.nil? }
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       m.reply(_("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") %
183               {:bold => Bold}.merge(vid))
184     else
185       m.reply(_("couldn't retrieve video info") % {:id => id})
186     end
187   end
188
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})
196       return
197     end
198     debug "filtering XML"
199     vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']]
200     debug vids
201     case vids.length
202     when 0
203       m.reply _("no videos found for %{what}") % {:what => what}
204       return
205     when 1
206       show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first
207       m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show}
208     else
209       idx = 0
210       shorts = vids.inject([]) { |list, el|
211         idx += 1
212         list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el))
213       }.join(" | ")
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)
219         }
220       end
221     end
222   end
223
224 end
225
226 plugin = YouTubePlugin.new
227
228 plugin.map "youtube info :movie", :action => 'info', :threaded => true
229 plugin.map "youtube [search] *words", :action => 'search', :threaded => true