#-- vim:sw=2:et #++ # # :title: YouTube plugin for rbot # # Author:: Giuseppe "Oblomov" Bilotta # # Copyright:: (C) 2008 Giuseppe Bilotta class YouTubePlugin < Plugin YOUTUBE_SEARCH = "http://gdata.youtube.com/feeds/api/videos?vq=%{words}&orderby=relevance" YOUTUBE_VIDEO = "http://gdata.youtube.com/feeds/api/videos/%{id}" YOUTUBE_VIDEO_URLS = %r{youtube.com/(?:watch\?(?:.*&)?v=|v/)(.*?)(&.*)?$} Config.register Config::IntegerValue.new('youtube.hits', :default => 3, :desc => "Number of hits to return from YouTube searches") Config.register Config::IntegerValue.new('youtube.descs', :default => 3, :desc => "When set to n > 0, the bot will return the description of the first n videos found") Config.register Config::BooleanValue.new('youtube.formats', :default => true, :desc => "Should the bot display alternative URLs (swf, rstp) for YouTube videos?") def help(plugin, topic="") 'youtube [search] : searches youtube videos | youtube info : returns description and video links' end def youtube_filter(s) loc = Utils.check_location(s, /youtube\.com/) return nil unless loc if s[:text].include? ' content) elsif s[:text].include? '' vids = @bot.filter(:"youtube.search", s)[:videos] if !vids.empty? return nil # TODO end end # otherwise, just grab the proper div if defined? Hpricot content = (Hpricot(s[:text])/".watch-video-desc").to_html.ircify_html end # suboptimal, but still better than the default HTML info extractor dm = /]*>/.match(s[:text]) content ||= dm ? dm.post_match.ircify_html : '(no description found)' return {:title => s[:text].ircify_html_title, :content => content} end def youtube_apivideo_filter(s) # This filter can be used either e = s[:rexml] || REXML::Document.new(s[:text]).elements["entry"] # TODO precomputing mg doesn't work on my REXML, despite what the doc # says? # mg = e.elements["media:group"] # :title => mg["media:title"].text # fails because "media:title" is not an Integer. Bah vid = { :formats => [], :author => (e.elements["author/name"].text rescue nil), :title => (e.elements["media:group/media:title"].text rescue nil), :desc => (e.elements["media:group/media:description"].text rescue nil), :cat => (e.elements["media:group/media:category"].text rescue nil), :seconds => (e.elements["media:group/yt:duration/"].attributes["seconds"].to_i rescue nil), :url => (e.elements["media:group/media:player/"].attributes["url"] rescue nil), :rating => (("%s/%s" % [e.elements["gd:rating"].attributes["average"], e.elements["gd:rating/@max"].value]) rescue nil), :views => (e.elements["yt:statistics"].attributes["viewCount"] rescue nil), :faves => (e.elements["yt:statistics"].attributes["favoriteCount"] rescue nil) } if vid[:desc] vid[:desc].gsub!(/\s+/m, " ") end if secs = vid[:seconds] vid[:duration] = Utils.secs_to_short(secs) else vid[:duration] = _("unknown duration") end e.elements.each("media:group/media:content") { |c| if url = (c.attributes["url"] rescue nil) type = c.attributes["type"] rescue nil medium = c.attributes["medium"] rescue nil expression = c.attributes["expression"] rescue nil seconds = c.attributes["duration"].to_i rescue nil fmt = case num_fmt = (c.attributes["yt:format"] rescue nil) when "1" "h263+amr" when "5" "swf" when "6" "mp4+aac" when nil nil else num_fmt end vid[:formats] << { :url => url, :type => type, :medium => medium, :expression => expression, :seconds => seconds, :numeric_format => num_fmt, :format => fmt }.delete_if { |k, v| v.nil? } if seconds vid[:formats].last[:duration] = Utils.secs_to_short(seconds) else vid[:formats].last[:duration] = _("unknown duration") end end } debug vid return vid end def youtube_apisearch_filter(s) vids = [] title = nil begin debug s.inspect doc = REXML::Document.new(s[:text]) title = doc.elements["feed/title"].text doc.elements.each("*/entry") { |e| vids << @bot.filter(:"youtube.apivideo", :rexml => e) } debug vids rescue => e debug e end return {:title => title, :vids => vids} end def youtube_search_filter(s) # TODO # hits = s[:hits] || @bot.config['youtube.hits'] # scrap the videos return [] end # Filter a YouTube video URL def youtube_video_filter(s) id = s[:youtube_video_id] if not id url = s.key?(:headers) ? s[:headers]['x-rbot-location'].first : s[:url] debug url id = YOUTUBE_VIDEO_URLS.match(url).captures.first rescue nil end return nil unless id debug id url = YOUTUBE_VIDEO % {:id => id} resp = @bot.httputil.get_response(url) xml = resp.body unless resp.kind_of? Net::HTTPSuccess debug("error looking for movie %{id} on youtube: %{e}" % {:id => id, :e => xml}) return nil end debug xml begin return @bot.filter(:"youtube.apivideo", DataStream.new(xml, s)) rescue => e debug e return nil end end def initialize super @bot.register_filter(:youtube, :htmlinfo) { |s| youtube_filter(s) } @bot.register_filter(:apisearch, :youtube) { |s| youtube_apisearch_filter(s) } @bot.register_filter(:apivideo, :youtube) { |s| youtube_apivideo_filter(s) } @bot.register_filter(:search, :youtube) { |s| youtube_search_filter(s) } @bot.register_filter(:video, :youtube) { |s| youtube_video_filter(s) } end def info(m, params) movie = params[:movie] id = nil if movie =~ /^[A-Za-z0-9]+$/ id = movie.dup end vid = @bot.filter(:"youtube.video", :url => movie, :youtube_video_id => id) if vid str = _("%{bold}%{title}%{bold} [%{cat}] %{rating} @ %{url} by %{author} (%{duration}). %{views} views, faved %{faves} times. %{desc}") % {:bold => Bold}.merge(vid) if @bot.config['youtube.formats'] and not vid[:formats].empty? str << _("\n -- also available at: ") str << vid[:formats].inject([]) { |list, fmt| list << ("%{url} %{type} %{format} (%{duration} %{expression} %{medium})" % fmt) }.join(', ') end m.reply str else m.reply(_("couldn't retrieve video info") % {:id => id}) end end def search(m, params) what = params[:words].to_s searchfor = CGI.escape what url = YOUTUBE_SEARCH % {:words => searchfor} resp = @bot.httputil.get_response(url) xml = resp.body unless resp.kind_of? Net::HTTPSuccess m.reply(_("error looking for %{what} on youtube: %{e}") % {:what => what, :e => xml}) return end debug "filtering XML" vids = @bot.filter(:"youtube.apisearch", DataStream.new(xml, params))[:vids][0, @bot.config['youtube.hits']] debug vids case vids.length when 0 m.reply _("no videos found for %{what}") % {:what => what} return when 1 show = "%{title} (%{duration}) [%{desc}] @ %{url}" % vids.first m.reply _("One video found for %{what}: %{show}") % {:what => what, :show => show} else idx = 0 shorts = vids.inject([]) { |list, el| idx += 1 list << ("#{idx}. %{bold}%{title}%{bold} (%{duration}) @ %{url}" % {:bold => Bold}.merge(el)) }.join(" | ") m.reply(_("Videos for %{what}: %{shorts}") % {:what =>what, :shorts => shorts}, :split_at => /\s+\|\s+/) if (descs = @bot.config['youtube.descs']) > 0 vids[0, descs].each_with_index { |v, i| m.reply("[#{i+1}] %{title} (%{duration}): %{desc}" % v, :overlong => :truncate) } end end end end plugin = YouTubePlugin.new plugin.map "youtube info :movie", :action => 'info', :threaded => true plugin.map "youtube [search] *words", :action => 'search', :threaded => true