Amazlet Tool for はてなダイアリー (2)

前回は CGI でちまちま作りましたが、今回はそれを MVC でオブジェクト指向なプログラムに書き直してみます。お手軽なウェブアプリなら Catalyst が一番です。しかし、Catalyst と SOAP::Lite は 相性が悪いらしいので、さらにお手軽に Net::Amazon を使ってみます。
モデルは DBD::Amazon を使うというディープな方法もありますが、今回はモデルは使いません。ビューは Template::Tookit を使います。コントローラは最近の Catalyst だと最初からついてくるルートコントローラを使います。
自分で動かしてみたい方用にソース一式を こちら においておきます。
アプリケーションの作成
catalyst.pl Amazlet cd Amazlet/script ./amazlet_create.pl view TT TT ./amazlet_server -r
ファイル構成
./Amazlet/script/amazlet_server.pl ./Amazlet/script/amazlet_create.pl ./Amazlet/lib/Amazlet/View/TT.pm ./Amazlet/lib/Amazlet/Controller/Root.pm ./Amazlet/lib/Amazlet.pm ./Amazlet/root/template/view.html ./Amazlet/root/template/list.html ./Amazlet/root/template/search.html ./Amazlet/amazlet.yml
TT.pm
package Amazlet::View::TT; use strict; use base 'Catalyst::View::TT'; 1;
Root.pm
package Amazlet::Controller::Root;
use strict;
use warnings;
use base 'Catalyst::Controller';
use Encode;
use Net::Amazon;
use Data::Dumper;
__PACKAGE__->config->{namespace} = '';
my $label_hash = {
'books-jp' => '和書',
'books-us' => '洋書',
'music-jp' => 'ポピュラー音楽',
'classical-jp' => 'クラシック',
'dvd-jp' => 'DVD',
'vhs-jp' => 'ビデオ',
'electronics-jp' => 'エレクトロニクス',
'software-jp' => 'ソフトウェア',
'videogames-jp' => 'ゲーム',
'kitchen-jp' => 'ホームキッチン',
'toys-jp' => 'おもちゃ&ホビー',
'sporting-goods-jp' => 'スポーツ',
};
sub default : Private {
my ( $self, $c ) = @_;
$c->forward('list');
}
sub list : Local {
my ( $self, $c ) = @_;
if ( $c->req->param('keyword') && $c->req->param('search_type') ) {
# 検索開始
my $service = Net::Amazon->new(
token => $c->config->{developer_token},
locale => 'jp',
);
my $keyword = $c->req->param('keyword');
Encode::from_to( $keyword, 'euc-jp', 'utf8' );
my $results = $service->search(
keyword => $keyword,
mode => $c->req->param('search_type')
);
# 検索結果を stash に詰める
foreach my $detail ( @{ $results->{xmlref}->{Details} } ) {
push( @{ $c->stash->{items} }, $self->gen_hash( $c, $detail ) );
}
}
$c->stash->{template} = 'template/list.html';
}
sub view : Local {
my ( $self, $c ) = @_;
if ( $c->req->param('asin') && $c->req->param('search_type') ) {
# 検索開始
my $service = Net::Amazon->new(
token => $c->config->{developer_token},
locale => 'jp',
);
my $results = $service->search( asin => $c->req->param('asin') );
# 検索結果を stash に詰める
foreach my $detail ( @{ $results->{xmlref}->{Details} } ) {
$c->stash->{item} = $self->gen_hash( $c, $detail );
}
}
$c->stash->{template} = 'template/view.html';
}
sub end : Private {
my ( $self, $c ) = @_;
$c->stash->{template} = 'template/list.html'
unless ( exists $c->stash->{template} );
# 検索タイプを stash に詰める
for my $item ( sort keys %{$label_hash} ) {
push(
@{ $c->stash->{search_types} },
{ id => $item, name => $label_hash->{$item} }
);
}
$c->forward( $c->view('TT') ) unless $c->response->body;
}
sub gen_hash {
my ( $self, $c, $detail ) = @_;
my $hash;
# UTF-8 から EUC-JP への変換
for my $item (
qw(ListPrice OurPrice UsedPrice ImageUrlSmall ImageUrlMedium ImageUrlLarge Availability Catalog Asin Url Manufacturer ProductName)
) {
if ( exists $detail->{$item} ) {
if ( $item eq 'Catalog' ) {
$hash->{$item} = $label_hash->{ $c->req->param('search_type') };
} else {
$hash->{$item} = Encode::encode( 'euc-jp', $detail->{$item} );
}
} else {
$hash->{$item} = '不明';
}
}
return $hash;
}
1;
Amazlet.pm
package Amazlet; use strict; use warnings; use Catalyst qw/-Debug StackTrace ConfigLoader Charsets::Japanese ConfigLoader Static::Simple/; our $VERSION = '0.01'; __PACKAGE__->config( name => 'Amazlet' ); __PACKAGE__->setup; 1;
view.html
<html> <head> <title>Amazlet Tool for はてなダイアリー</title> <link rel="stylesheet" type="text/css" href="http://d.hatena.ne.jp/theme/hatena/hatena.css" /> </head> <body> <form method="post" action="/list"> <!-- 検索フォーム --> [% INCLUDE template/search.html -%] <!-- 詳細モード --> <div class="hatena-asin-detail"><a href="http://www.amazlet.com/browse/ASIN/[% c.stash.item.Asin %]/[% c.config.affiliate_id %]"><img src="[% c.stash.item.ImageUrlMedium %]" class="hatena-asin-detail-image" alt="[% c.stash.item.ProductName %]" title="[% c.stash.item.ProductName %]"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazlet.com/browse/ASIN/[% c.stash.item.Asin %]/[% c.config.affiliate_id %]">[% c.stash.item.ProductName %]</a></p><ul><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> [% c.stash.item.Manufacturer %]</li><li><span class="hatena-asin-detail-label">価格:</span> [% c.stash.item.ListPrice %]</li><li><span class="hatena-asin-detail-label">メディア:</span> [% c.stash.item.Catalog %]</li></ul></div><div class="hatena-asin-detail-foot"></div></div> <!-- 詳細モード --> <pre> <div class="hatena-asin-detail"><a href="http://www.amazlet.com/browse/ASIN/[% c.stash.item.Asin %]/[% c.config.affiliate_id %]"><img src="[% c.stash.item.ImageUrlMedium %]" class="hatena-asin-detail-image" alt="[% c.stash.item.ProductName %]" title="[% c.stash.item.ProductName %]"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazlet.com/browse/ASIN/[% c.stash.item.Asin %]/[% c.config.affiliate_id %]">[% c.stash.item.ProductName %]</a></p><ul><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> [% c.stash.item.Manufacturer %]</li><li><span class="hatena-asin-detail-label">価格:</span> [% c.stash.item.ListPrice %]</li><li><span class="hatena-asin-detail-label">メディア:</span> [% c.stash.item.Catalog %]</li></ul></div><div class="hatena-asin-detail-foot"></div></div> </pre> </body> </html>
list.html
<html>
<head>
<title>Amazlet Tool for はてなダイアリー</title>
<link rel="stylesheet" type="text/css" href="http://d.hatena.ne.jp/theme/hatena/hatena.css" />
</head>
<body>
<form method="post" action="/list">
<!-- 検索フォーム -->
[% INCLUDE template/search.html -%]
<!-- 一覧 -->
[% FOREACH item = c.stash.items -%]
<table border="1" width="80%">
<tr>
<td>
<div class="hatena-asin-detail"><a href="http://www.amazlet.com/browse/ASIN/[% item.Asin %]/[% c.config.affiliate_id %]"><img src="[% item.ImageUrlMedium %]" class="hatena-asin-detail-image" alt="[% item.ProductName %]" title="[% item.ProductName %]"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="http://www.amazlet.com/browse/ASIN/[% item.Asin %]/[% c.config.affiliate_id %]">[% item.ProductName %]</a></p><ul><li><span class="hatena-asin-detail-label">出版社/メーカー:</span> [% item.Manufacturer %]</li><li><span class="hatena-asin-detail-label">価格:</span> [% item.ListPrice %]</li><li><span class="hatena-asin-detail-label">メディア:</span> [% item.Catalog %]</li></ul></div><div class="hatena-asin-detail-foot"></div></div><div align="center"><a href="/view?asin=[% item.Asin %]&search_type=[% c.req.param('search_type') %]">リンクを作成</a></div>
</td>
</tr>
</table>
[% END -%]
</body>
</html>
search.html
<table border="1" width="80%">
<tr>
<th>キーワード</th>
<td>
<input type="text" name="keyword" size="40" value="[% c.req.param('keyword') %]" />
</td>
</tr>
<tr>
<th>検索種別</th>
<td>
<select name="search_type">
[% FOREACH type = c.stash.search_types -%]
[% IF c.req.param('search_type') == type.id -%]
<option value="[% type.id %]" selected>[% type.name %]</option>
[% ELSE -%]
<option value="[% type.id %]">[% type.name %]</option>
[% END -%]
[% END -%]
</select>
</td>
</tr>
<tr>
<th colspan="2">
<input type="submit" name="submit" value="検索" />
<input type="reset" value="クリア">
</th>
</tr>
</table>
</form>
amazlet.yml
--- name: Amazlet charsets: EUC-JP amazon_wdsl: http://soap.amazon.co.jp/schemas3/AmazonWebServices.wsdl developer_token: xxxxxx affiliate_id: xxxxxx