読者です 読者をやめる 読者になる 読者になる

PerlerのRuby日記

Rubyとか

Youtube検索

WEB+DB PRESS Vol.44より、MVCの解説としてYoutubeから検索するプログラムをPerlで例として掲載されていたのをRubyで書き直してみた。

忘れないようにメモ。

少しだけ簡単にはしたけれど、まあほぼ同じ。

index.cgi
#!/home/rightgo09/bin/ruby
# -*- coding: utf-8 -*-

require 'cgi'

$LOAD_PATH.unshift('lib')
require 'myapp'

q   = CGI.new
app = MyApp.new

app.request = q
app.path    = q.path_info

res = app.dispatch

print q.header({
  'charset' => 'UTF-8',
  'type'    => res.content_type,
});
print res.content

__END__
lib/myapp.rb
require 'myapp/view'
require 'myapp/response'
require 'myapp/controller'

class MyApp
  attr_accessor :request, :path

  def dispatch
    con = self.controller
    
    con.req  = self.request
    con.res  = Response.new
    con.view = View.new({
      'path_segments' => [ self.path_segments ]
    })

    con.handle_request

    return con.res
  end

  # get controller instance
  def controller
    handler = ['myapp','controller', self.path_segments].join('/')
    require handler

    handler = self.get_class(self.get_classname(self.path_segments))

    return handler.new
  end

  def path_segments
    self.path ||= '/'

    path_segments = self.path.split('/')
    path_segments.shift
    path_segments.push('index') if path_segments.size == 0

    return path_segments
  end

  def get_classname(path_segments = Array.new)
    return ['controller', path_segments].join('::').gsub(/\w+/) do |m|
      m[0] = m[0,1].upcase; m
    end
  end

  def get_class(classname)
    return classname.split(/::/).inject(Object) do |c,name|
      c.const_get(name)
    end
  end
end
lib/myapp/response.rb
class Response
  attr_accessor :content_type, :content
end
lib/myapp/view.rb
require 'erb'
include ERB::Util

class View
  attr_accessor :path_segments

  def initialize(args)
    self.path_segments = args['path_segments']
    @vars = Hash.new
  end

  def vars(args = nil)
    if args.nil? then
      return @vars
    else
      @vars.merge!(args)
    end
  end

  def template_file
    file = ['template', self.path_segments].join('/')
    file << '.html'

    return file
  end

  def render
    erb = ERB.new(File.read(self.template_file))
    vars = self.vars
    out = erb.result(binding())

    return out
  end
end

うーん、bindingがいちいちローカル変数に落とし込んでおかないといけないのが気持ち悪い。

lib/myapp/model/youtube.rb
require 'open-uri'
require 'nokogiri'
require 'myapp/model/youtube/result'

YOUTUBE_API = 'http://gdata.youtube.com/feeds/api/videos?orderby=viewCount&vq='

class Model
  class Model::Youtube
    attr_accessor :title, :link, :thumbnail_url

    def initialize(args = Hash.new)
      self.title         = args[:title]
      self.link          = args[:link]
      self.thumbnail_url = args[:thumbnail_url]
    end

    def Youtube.search(query = '')
      uri = URI(YOUTUBE_API + URI.encode(query))
      entries = self._parse_videos(uri.read)
      return Model::Youtube::Result.new(entries)
    end

    def Youtube._parse_videos(content)
      doc = Nokogiri::XML(content)
      entries = Array.new
      doc.search('entry').each { |entry|
        entries.push(Model::Youtube.new({
          :title => entry.search('title').text,
          :link  => entry.xpath('media:group/media:player').first['url'],
          :thumbnail_url => entry.xpath('media:group/media:thumbnail').first['url'],
        }));
      }
      return entries
    end

    def to_hash
      return {
        :title        => self.title,
        :link         => self.link,
        :tumbnail_url => self.thumbnail_url,
      }
    end
  end
end
lib/myapp/model/youtube/result.rb
class Model
  class Model::Youtube
    class Model::Youtube::Result
      attr_accessor :entries

      def initialize(entries)
        self.entries = entries
      end
    end
  end
end
lib/myapp/controller.rb
class Controller
  attr_accessor :req, :res, :view

  def handle_request
    self.do_task

    if self.res.content.nil? then
      # default content is rendering view
      self.res.content = self.view.render
    end

    self.res.content_type ||= 'text/html' # default content-type

    return self.res
  end
end
lib/myapp/controller/index.rb
class Controller::Index < Controller
  def do_task
  end
end
lib/myapp/controller/search.rb
require 'myapp/model/youtube'

class Controller::Search < Controller
  def do_task
    self.view.vars({
      'result' => Model::Youtube.search(self.req['q']),
      'q'      => self.req['q'],
    })
  end
end
template/index.html
<html>
<head>
<title>YouTube Search</title>
<style type="text/css">
ul li {
  list-style-type : none;
  display : inline;
}
</style>
</head>
<body>
<h1>YouTube Search</h1>
<form method="get" action="./index.cgi/search">
  <input type="text" name="q" value="" />
  <input type="submit" value="search" />
</form>
</body>
</html>
template/search.html
<%#-*- coding: UTF-8 -*-%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>YouTube Search - Result</title>
<style type="text/css">
ul li {
  list-style-type : none;
  display : inline;
}
</style>
</head>
<body>
<h1>YouTube Search</h1>
<form method="get">
  <input type="text" name="q" value="<%=h vars['q'] %>" />
  <input type="submit" value="search" />
</form>
<h2>検索結果 (<%= vars['result'].entries.size %>)</h2>
<ul>  
  <% for entry in vars['result'].entries %>
  <li><a href="<%= entry.link %>">
  <img src="<%= entry.thumbnail_url %>" title="<%=h entry.title %>" /></a></li>
  <% end %>
</ul>
</body>
</html>

先頭にUTF-8だと入れないとerbの置き換えで死ぬ。

その解決策でえらく時間かかった。



f:id:rightgo09_ruby:20101231233033p:image

f:id:rightgo09_ruby:20101231233034p:image


こんな感じ。