PerlでTwitter Streaming APIの出力をブラウザ上でニコニコ動画風に表示するスクリプト(再接続未実装)
スポンサードリンク
Twitter Streaming API接続を使い、ニコニコ動画風にブラウザ上で字幕を流すMojoliciousウェブアプリケーションの最新版です。
なにかイベントあった時にサクッとAmazon EC2を立ち上げて、ソースをコピー&ペーストしてすぐ設置できるぐらいのカジュアルさを目指しました。
■動作確認環境
- CentOS 5.6 / Perl 5.8.8 / Mojolicious 1.97
- Amazon Linux AMI / Perl 5.10.1 /Mojolicious 2.19(2.20~2.24は不可)
~~~
起動するには、事前に取得しておいたconsumer keyをconsumer_keys.yamlに、access tokenを~/.pit/defalt.yamlに設定しておき、
perl twitter-jimaku.pl daemon -listen http://*:3000
とサーバで実行したあと、ウェブブラウザ(Firefox7、Google Chrome 15)で
http://www.example.com:3000
に接続すると画面に字幕が流れる、というものになります。
~~~
現状わかっている不具合として、
「Twitter Streaming APIの接続が切れた時の再接続ができない」
というものがあります。いろいろ調べて、AnyEventのイベントループだけで廻っている場合は策があるのですが、Mojoliciousのイベントループとの複合技になると途端に制御が難しくなってしまいます。だれか、解決策をご存じの方にご指南をいただきたく、現状のソースを公開します。
Firelaneme: twitter-jimaku.pl
#!/usr/bin/perl
use AnyEvent::Twitter::Stream;
use Config::Pit;
use EV;
use File::Spec;
use FindBin::Real;
use Mojolicious::Lite;
use Mojo::JSON;
use utf8;
use version;
use YAML;
binmode STDOUT => ':utf8';
my $keys = YAML::LoadFile(
File::Spec->catdir( FindBin::Real::Bin(), '..', 'consumer_keys.yaml' )
);
my $pit = pit_get( 'twitter.com@CLCLCL' );
# Mojoliciousサーバに接続されたクライアント
my $clients = {};
my $listener = AnyEvent::Twitter::Stream->new(
consumer_key => $keys->{consumer_key},
consumer_secret => $keys->{consumer_key_secret},
token => $pit->{access_token},
token_secret => $pit->{access_token_secret},
method => 'sample',
#method => 'userstream',
#method => 'filter',
#track => 'JAPAN',
on_tweet => sub {
my $tweet = shift; # $tweetはflagged utf8
return if rand(1) < 0.8; # sample streamは流量多いので2割だけ採用
my $user = $tweet->{user}{screen_name};
my $text = $tweet->{text} || '';
my $lang = $tweet->{user}{lang} || '';
return if $lang ne 'ja'; # 日本語のみ
return unless $user && $text; # たまに空のTweetが流れるのを防ぐ
$text =~ s/</</g;
$text =~ s/>/>/g;
$text =~ s/[\x00-\x1f\x7f]//g; # コントロールコード除去
# 伏せ字処理
my $fuseji = sub {
# ユーザー名を伏せ字
$user =~ s/./*/g;
# ツイート内容の名前らしきところを伏せ字&撹乱
$text =~ s/\@[^\s:]+/@****/g;
$text =~ s/[^\s]+さん/****ちゃん/g;
$text =~ s/[^\s]+ちゃん/****さん/g;
$text =~ s/[^\s]+くん/****クン/g;
# URLを伏せ字
$text =~ s{http://t.co/[\w\-]+}{http://t.co/******}g;
};
$fuseji->(); # 伏せ字しないときはコメントアウトすること
print "$user : $text\n"; # コンソールに表示
my $json = Mojo::JSON->new;
# JSONに変換
my $str = $json->encode({
user => $user,
text => $text,
});
# Mojo::JSONするとutf8 fragged落ちるのでutf8 fraggedにする
utf8::decode( $str );
# Mojoliciousサーバに接続中の全てのクライアントに送信
for (keys %$clients) {
$clients->{$_}->send_message( $str );
}
},
on_error => sub {
my $error = shift;
warn "ERROR: $error";
},
on_eof => sub {
},
);
get '/' => 'index';
websocket '/stream' => sub {
my $self = shift;
# ログに接続記録
app->log->debug(sprintf 'Client connected: %s', $self->tx);
# $clientsにWebSocketクライアントを追加
my $id = sprintf "%s", $self->tx;
$clients->{$id} = $self->tx;
# WebSocketコネクション切断時処理(共通)
my $on_finish = sub {
# ログに切断記録
app->log->debug('Client disconnected');
# $clientsからWebSocketクライアントを削除
delete $clients->{$id};
};
# イベント設定
if ( $Mojolicious::VERSION < qv("v2.0") ) {
# Mojolicious 1.xの時の処理
$self->on_message();
$self->on_finish( $on_finish );
}
else {
# Mojolicious 2.xの時の処理
$self->on(
message => sub {},
finish => $on_finish,
);
}
};
# Mojolicious::Liteのイベントループ開始
app->start;
__DATA__
@@ index.html.ep
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mojolicious + WebSocket + Twitter Streaming API</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0" />
%= javascript '/js/jquery.js';
<script type="text/javascript">
// WebSocketの書き方の違いを吸収するオブジェクト
WS = function(f) {
if (typeof MozWebSocket != 'undefined' ) return new MozWebSocket(f);
if (typeof WebSocket != 'undefined' ) return new WebSocket(f);
return;
}
// onload
$(function () {
// WebSocketオブジェクト作成
var ws = WS('<%= url_for('stream')->to_abs %>');
// connectingを表示
var html = ''
+ '<p class="description">'
+ '<img src="data:image/gif;base64,'
+ 'R0lGODlhEAAQAPfgAP////39/erq6uvr6+jo6P'
+ 'n5+dPT0/v7+/X19efn5/Pz8/j4+Pf39/r6+vz8'
+ '/MzMzO/v7/b29svLy/7+/unp6e7u7kJCQtnZ2f'
+ 'Hx8a+vr4mJid7e3s/PzyYmJrOzs/Dw8NLS0vT0'
+ '9Le3t9ra2tvb25CQkKOjo2tra9DQ0KysrM3Nza'
+ '2traurq729vezs7M7OzuHh4fLy8rq6und3d6Cg'
+ 'oIGBgYCAgGRkZGJiYsPDw8fHx4eHh+Dg4J+fn6'
+ 'KiooiIiG9vb6enp9fX18DAwOXl5d3d3e3t7WBg'
+ 'YJmZmZOTk9/f30VFRebm5jQ0NBUVFQQEBNjY2I'
+ 'SEhOTk5K6urtzc3D8/P2dnZ8LCwpubm8jIyLm5'
+ 'uZqamiEhIcTExC0tLbCwsIyMjNXV1dHR0VxcXO'
+ 'Pj40lJSTw8PGxsbExMTCwsLF9fXxAQEMnJyRYW'
+ 'FpSUlCIiIhsbGwgICAsLC11dXVhYWJGRkba2tr'
+ 'y8vMbGxr+/v7i4uDs7O76+vmFhYYaGho2NjbW1'
+ 'tZeXl4qKiiQkJKmpqYODg0ZGRk9PT3Z2dgkJCT'
+ 'o6OkFBQY+Pjx8fH3l5eRMTEw8PDyoqKrGxsWho'
+ 'aHNzcwcHB7KysqGhoYKCgkpKSmVlZXFxcaioqE'
+ '1NTeLi4p2dnaampqSkpJ6ensXFxVNTU7S0tFZW'
+ 'VjExMVlZWaWlpVRUVDAwMCgoKFBQUKqqqg0NDU'
+ 'NDQxkZGT09PUdHR3p6ehISEgICAsHBwURERDU1'
+ 'NZKSkm1tbTk5OWlpaRwcHFJSUtTU1DMzMyAgIH'
+ '5+fiMjI3JycnR0dA4ODkhISMrKynx8fJiYmAYG'
+ 'BnV1dU5OTgMDA4WFhR4eHgoKCpycnC8vL1paWm'
+ 'NjYzc3N7u7u4uLiycnJ3t7e15eXhoaGjY2NkBA'
+ 'QP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
+ 'AAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEA'
+ 'AAAh+QQFAADgACwAAAAAEAAQAAAIpQDBCRxIsG'
+ 'DBF1FwOQEQwEEAg+B6XJMT5wmAAwwiFCjo480j'
+ 'TVOYAJhQAEMFBgPFLOomyCADAQI2gqvDBQhEcB'
+ 'VgVBA4p4OImyFIeBIoy4uAmwcMhBFoocmAmw0k'
+ 'cBB4Yk+emwJyGBDYw8KPmyhkbBB4wUonTgYNTB'
+ 'nyYaCeMaiQqMCg4EILGimKFLzj6MYZRDY0JGFx'
+ 'AaISD0lqaEil4+jNxwIDAgAh+QQFAADgACwBAA'
+ 'EADgAOAAAImwDBCTRQx1SkDmj8qBDIkIUzbVzg'
+ 'OFkj59QWhhmqrJohggKBLzgqrQEADsocRRcZCq'
+ 'wBIMAEHxaiqFQZoMCBGWWuzGQYAAGDOa0q7BQ4'
+ '4cOHG3QgDAUXQMCAHUckLEVAZoClSTSWJqBSAc'
+ 'YOY3d2EhFThAE4HTVsWBqBIAKTMKNeuGD4AAkY'
+ 'N5+CfNGSjMDMBDokgVqRY0QMhgEBACH5BAUAAO'
+ 'AALAEAAQAOAA4AAAiZAMEJHOEDCDILOJKAEMhQ'
+ 'xpkyFvY08dLBkAmGfPqo+nPFxQAtlBp1oAGOhz'
+ 'I1KRgy/NOG1wtAk6apVGnlGDQ3QDjMZJgh0RJM'
+ 'M2LsFJjgSRsNNhQMBQegaaofUJYGOOAATwkZSx'
+ 'dEOECBExYUOxFUUBAAnBBQQSQkKNAAgwAiAxYw'
+ 'JCHDg4wcEgyQYIJgJoQRKrJwKOJCrsCAACH5BA'
+ 'UAAOAALAEAAQAOAA4AAAiZAMEJhOFBg5UjtExA'
+ 'Ecgwy48TN8aoQrNETQaGDwrNMKECQoUufsx8Yw'
+ 'EuwZYafBgyxHLqkAEdYDyoVDmjQ50MSUbMZChC'
+ 'mCkTWBDsFEghFitCJiIMBUfg0aA8LKQszfAqkx'
+ 'APKJYeiRPlw6gWPHZOsOXlATgieLLwwOAgQIMC'
+ 'DQIsY0ghDIgLPBIYUbAgwEwEAqSQoYChL8OAAC'
+ 'H5BAUAAOAALAEAAQAOAA4AAAiZAMEJFMDGFSMN'
+ 'SPTAEMjwwopAJX7YmAGkxhCGRVJcykNCgQIQlz'
+ 'RZuQPuQ4sUBhgyzIAKCAkqdl6oVFkCTSgOLQjM'
+ 'ZJhjySY2XQrsFOjCTBkOEhoMBTegiQUqIDAs1Z'
+ 'Kmz4ALOoduGqRrARkYMXYKggMLBLgQCQSEODAB'
+ 'wAprtd74YMjgA4YIBwA8SeStx0wHBQrktVBIBc'
+ 'OAACH5BAUAAOAALAEAAQAOAA4AAAibAMEJjEFF'
+ 'R6kVIh5QEMiQwIMWdjIE6RHIBwqGLl7gEUKAQQ'
+ 'Ql2MCAeQCOAQkURBgyzGGjBBkjF1KqZEiIkggC'
+ 'GxTMZIjixJ8EUhzsFPgBx4kBAgIMBQeBzo0YEB'
+ 'os7XJo24IQBZb6MRQqQIECE3Zu2aMGCrgAAQBw'
+ 'm5KAAKBm1KpkYAggDTNpkJz4ItaJxcwHhWZx6U'
+ 'CqhAGGAQEAIfkEBQAA4AAsAQABAA4ADgAACJkA'
+ 'wQksYAQGMA4GlGAQyBABgQ0XQEjo0uKKEoYLBj'
+ 'BxoeBAgwEGPEgiAc5BDCMIGDIUEuTLgAYhIqhU'
+ 'eQWLhAYMHMxkWCQJCwcHAOwUGEJDCQBIh4JTYE'
+ 'PDoicplIpBhARTHBxKRZ0RoSIYpB87UxwZxgOc'
+ 'qEZtdtkRMGBItl99+DCkUSXaoDRNzCzpJWOmmB'
+ 'JjzFg4QWMEw4AAIfkEBQAA4AAsAQABAA4ADgAA'
+ 'CJkAwQmc0AABhAEDICwQyHCCAwYhIAiQsmFDBY'
+ 'ZIAAQ44GBCgAgUwhgQAO6Bl2cAGDIkIIGDgiiV'
+ 'jqhUOWLIhjJypsxkSEFLljdrEuwUuOALoA5OCA'
+ 'wFFyHIClJwSi3d8EkEIy7FlupxIwFEpkiBdg7Z'
+ '0UMpIUW5atwyAuGBCUc7XjBcUa2KoUN0cJwQxG'
+ 'amEBqIxtzY4cETw4AAOw=='
+ '" width="16" height="16" style="vertical-align: middle;" />'
+ 'CONNECTING</p>'
+ '<p>Google Chrome 14以上、Mozilla Firefox 7以上で接続できます</p>';
connecting = $('<div id="connecting">')
.html(html)
.appendTo('body');
// テロップ準備
var jimaku = new Stage('#stage');
// WebSocket接続時
ws.onopen = function () {
// connectingを消す
connecting.fadeOut(1500);
// Welcomeを表示・自動で消える
var html = ''
+ '<p class="description">'
+ 'WELCOME TO<br />TEST</p>'
+ '<p class="icons">'
+ '<img class="icon_left" alt="test" />'
+ '<img class="icon_right" '
+ 'src="'
+ 'data:image/png;base64,'
+ 'iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAA'
+ 'COEfKtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ'
+ 'bWFnZVJlYWR5ccllPAAAA5RJREFUeNrsW+tx2k'
+ 'AQlpj8lzsQqcBKBaIDkQpEKjAdQCqwUwFxBeAK'
+ 'pFSAUwGkApEKlFtmRYSQ0aF7i/1mdhjbMrf6tL'
+ 'cv7XkegUAgEAgEAoFAIBAIBOdRlmXAJGWyYvJI'
+ 'jHQT9shkyyRG0irA70ImCZMF/J3YuiQPyCmQsK'
+ 'I8x67xc0aMnZOXlvwAclNi7Xzb8pC2RqIDFXr4'
+ 'jpIXso93Jg9XLtvgNTl8+r7/15an/ow3YFKPbX'
+ 'k7tiotkVfx2LRCGE1FYM4XYkrQppCWfIvT7/Ei'
+ 'M2KNLanBxTZRuHZWykWhPdHm3EIFXhdKXDcp1U'
+ 'AviVgmFTcouJJBZIflyyAx1Eli0tPnxD3XS0v1'
+ '2Oreys8Czju2yPrqWNjoD4WIbKROOhDqJjG50S'
+ 'e2ERle+f61ZgJXWko5jFwRkzGTCUpfHJjMWbn1'
+ '2lKy7Q2kvGOmyx9XEtoPn76AnxXFkw4LLDqK+b'
+ '6ABsAEin+Fa3TqwNb/wnPhSGCRn4qUB7eQoxWY'
+ 'IO+oA0+ZBztRxAID9E+mblI1wCf/aPHJEfr7KR'
+ 'iRL+gLE+y73SOO21xkC3vsC97wSRzujbwq6xiJ'
+ 'fhOSGCn0iTYBDGUJlld1uKW29NFHzNEqxwMhbY'
+ '8WB65q03w1MJJIXoDERQMir9qu70jkKTpjVyr2'
+ 'JZEHVUk+4Ij8YSonywKXd0ZeVXp+k2WBPK8Zh+'
+ 'IPIVi+SA8iA8wJc5RTIGGk/ZZWC1/xhZuBBJEp'
+ 'pmhqmgkd0XiGEjlM4APPNIPS0Q4kM0JxKdBAvv'
+ 'eV58JPKrXAlhRs5xfX0hPue1SticGeXu9Iyx78'
+ 'Z96LR4rJix1MbZY37TINPvAwVOtTboEYxWYOET'
+ 'i3Uit8t7su7ca6l5Ec+/otGbZCMm1sPICbGfeZ'
+ 'YoUtPNM1/o+jbzbWzFOhEWCc61soJC5onN+wCU'
+ '+ybnCHEsu2Oo3DQUrHODp9U23GJROZNK0dtdpZ'
+ 'HDRWShx8Y1Comn9Ou6Y48X9TB6KtVPL8HlFy75'
+ '0P/Iwda1/BW7XvygisVRC54+2otlTlYgJMFKMr'
+ 'FcTEG8673mpg6VX7yhDmBQcoTWNh/PHhAZvMMe'
+ 'Iy6w5bO5CaVGeEE6sdCg6X20ak0lNSKi1yazqn'
+ 'M3mUX+ZoB/T9dAwVHTDFah32cZLAFjInKDIGjQ'
+ '7e/4PTOSPsl027UMdLpaBGZEVm1FLpVETVqx07'
+ 'TpoTCAQCgUAgEAgEAoFAOOGfAAMAIv2Ryl/695'
+ 'gAAAAASUVORK5CYII='
+ '" alt="Twitter" /></p>';
$('<div id="welcome">')
.html(html)
.hide()
.delay(1000)
.fadeIn(500)
.delay(5000)
.fadeOut(1500)
.appendTo('body');
// デバッグコンソールに接続完了を表示
console.log('Connection opened');
};
// WebSocketサーバからメッセージ受信
ws.onmessage = function (msg) {
// サーバからメッセージ受け取る
var res = JSON.parse(msg.data);
// テロップ流す
var d = jimaku.create(res.text);
};
});
// テロップオブジェクト(流れる1本の字幕)
var Telop = function( stage, msg, y ) {
this.stage = stage;
this.msg = msg;
this.position = {};
this.position.x = 0;
this.position.y = y;
this.screen = {};
this.screen.width = $( this.stage ).width() || 100;
this.die = false; // checkで変化
this.width = 0; // createで決定
this.speed = 0; // createで決定
this.create();
};
Telop.prototype = {
slice: 17, // 流れる速さ 17msec
time: 5000, // 画面を流れ切る時間 5000msec
create : function() {
this.object = $('<span>')
.css('visiblity','hidden')
.html(this.msg)
.appendTo( this.stage );
this.position.x = this.screen.width - 100;
this.width = this.object.width() + 1;
this.speed = ( this.screen.width + this.width ) / (this.time / this.slice );
this.show();
},
do_: function() { // 1フレーム分動かす処理
// doにするとOperaとSafariがdo{}whire()と勘違いしてパースエラー
this.move();
this.show();
this.check();
},
// move: 1フレーム分の動き方指定
move: function() {
this.position.x -= this.speed;
},
// show: オブジェクトを表示/再表示
show: function() {
this.object.css({
visiblity: 'visible',
display : 'block',
width : this.width + 'px',
left : this.position.x + 'px',
top : this.position.y + 'px'
});
},
// check: 1フレーム分動いた後の生死判定
check: function() {
if ( this.position.x < this.width * -1) {
this.die = true;
this.object.remove();
}
},
// isDie: メンバプロパティisDie(生死状態)アクセサ
isDie: function() {
return this.die;
},
// getPosition: メンバプロパティPosition(位置)アクセサ
getPosition: function() {
return this.position;
},
// getPosition: メンバプロパティwidth(幅)アクセサ
getWidth: function() {
return this.width;
}
};
// ステージオブジェクト(テロップを一元管理)
var Stage = function( stage ) {
var self = this; // setIntervarl用
this.stage = stage; // Telopオブジェクトが入る要素指定
this.objects = []; // Telopオブジェクト格納用
this.width = $( this.stage ).width() || 1024; // ステージ幅
// タイマーobj(1つでステージ上のobj全部に動かす指示を出す)
this.id = setInterval( function(){ self.move(); }, 17);
}
Stage.prototype = {
// create: ステージにtelopオブジェクトを追加
// WebSocketでデータ受信したら呼ばれる
create: function( mes ) {
var h = {}; // ステージ上の空き位置探索用捨てハッシュ
var ny = 0; // 表示位置
// ステージ上のobjのy位置をキーにして一番右のtelopの位置記録
if ( this.objects.length != 0) {
for ( var i = 0; i < this.objects.length; i++) {
var item = this.objects[i];
h[ item.getPosition()['y'] ]
= item.getPosition()['x']
+ item.getWidth() + 100; // 100は経験値で追加
}
// 全ての段を上から見てステージの空き位置を探索
for ( var y = 0; y <30*40; y += 30 ) { // 30px刻みで 40段分探索
if ( (typeof h[y] == 'undefined' ) || ( h[ y ] < this.width ) ) {
ny = y;
break;
}
}
}
// ステージにテロップを挿入
this.objects.push( new Telop( this.stage, mes, ny ) );
},
// move: ステージ上のobjを全部動かす:タイマーで呼ばれる
move: function() {
// ステージ上のobjを順番に選択する
for ( var i = this.objects.length - 1; i > -1; i--) {
var item = this.objects[i];
item.do_(); // objに設定された動きをさせる
// ステージ上の死んだobjを取り除く
if ( item.isDie() ) { // objは死んでいるか?
// objを詰める(forを逆順にする必要あり)
this.objects.splice(i, 1);
}
}
}
}
</script>
<style type="text/css">
html, body { height: 98%; }
h1 { font-size: 14px; }
span {
position: absolute;
font-size: 24px;
font-family: 'A-OTF 新ゴ Pro M';
}
body {
overflow: hidden;
}
#welcome {
position: absolute;
border: 0px;
text-align: center;
width: 440px;
height: 267px;
padding-top: 64px;
-webkit-border-radius: 16px;
-moz-border-radius: 16px;
border-radius: 16px;
-webkit-box-shadow: #666 6px 6px 12px;
-moz-box-shadow: #666 6px 6px 12px;
box-shadow: #666 6px 6px 12px;
background: #66aaaa;
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#66aaaa), to(#4D8080));
background: -webkit-linear-gradient(#66aaaa, #4D8080);
background: -moz-linear-gradient(#66aaaa, #4D8080);
background: -ms-linear-gradient(#66aaaa, #4D8080);
background: -o-linear-gradient(#66aaaa, #4D8080);
background: linear-gradient(#66aaaa, #4D8080);
left: 50%;
top: 50%;
margin-left: -220px;
margin-top: -133px;
}
#welcome p.description {
width: 316px;
height: 120px;
background-color: black;
color: white;
margin: 0px auto 0px;
text-align: center;
font-size: 42px;
line-height: 52px;
padding-top: 16px;
font-family: Alial;
font-weight: bold;
text-shadow: 3px 3px 0px #888;
}
#welcome p.icons {
width: 385px;
height: 80px;
margin: 16px auto 0px;
text-align: center;
}
#welcome .icon_left {
width: 80px;
height: 80px;
float: left;
}
#welcome .icon_right {
width: 80px;
height: 80px;
float: right;
}
</style>
</head>
<body>
<header>
<h1>Mojolicious + WebSocket + Twitter Streaming API</h1>
</header>
<article id="stage">
</article>
</body>
</html>
スポンサードリンク
トラックバック(0)
トラックバックURL: http://blog.dtpwiki.jp/MTOS/mt-tb.cgi/3761





![: Amazon.co.jp: プラスティック・メモリーズ 1【完全生産限定版】(イベントチケット優先販売申込券付) [Blu-ray]](/lists/_9/B00VWX66E8.jpg)
![: Amazon.co.jp: プラスティック・メモリーズ 2【完全生産限定版】[Blu-ray]](/lists/_9/B00VWX66K2.jpg)
![: Amazon.co.jp: プラスティック・メモリーズ 3【完全生産限定版】[Blu-ray]](/lists/_9/B00VWX6MV0.jpg)
![: Amazon.co.jp: プラスティック・メモリーズ 4【完全生産限定版】[Blu-ray]](/lists/_9/B00VWX66IO.jpg)
![: Amazon.co.jp: プラスティック・メモリーズ 5【完全生産限定版】[Blu-ray]](/lists/_9/B00VWX6Y0E.jpg)
![: Amazon.co.jp: プラスティック・メモリーズ 6【完全生産限定版】[Blu-ray]](/lists/_9/B00VWX69D6.jpg)


コメントする