2012年4月30日月曜日

字幕検索 その3

検索機能は、検索対象を番組情報と字幕情報の2つから選んで検索できるようにして利用しています。
番組情報の検索は、単純にLIKE演算子を使用して、字幕情報の検索ではブール全文検索を使用しています。

次のようにして字幕検索結果のリスト表示をしています。
my $query = 'バイク -自転車';
search_caption($query);

sub search_caption()
{
        my $query = shift @_;
        my $sql = <<END;
select
        c.dc_name,
        c.dc_ch,
        e.dv_start_time,
        date_format(addtime(e.dv_start_time, e.dv_duration), '%H%i%s'),
        e.dv_duration,
        e.dv_name,
        e.dv_favorite,
        timediff(a.dp_start_time, e.dv_start_time),
        time_to_sec(timediff(a.dp_start_time, e.dv_start_time)),
        a.dp_text
from
        da_caption as a
        inner join
                (da_event as e
                        inner join
                                da_channel as c
                        on
                                c.dc_network_id = e.dv_original_network_id
                        and
                                c.dc_service_id = e.dv_service_id
                )
        on
                a.dv_id = e.dv_id
where
        match(a.dp_text) against(? in boolean mode)
order by e.dv_start_time desc
END

        my $dsn = 'DBI:mysql:database=oneseg24;host=localhost';
        my $dbh = DBI->connect($dsn, 'user', 'password', {RaiseError => 1, AutoCommit => 0, mysql_enable_utf8 => 1});

        my $sth = $dbh->prepare($sql);
        my $rv = $sth->execute("${query}");

        my %program = ();
        my @order = ();

        while (my $ref = $sth->fetch()) {
                my @tmp = @$ref;

                my $key = "$tmp[1]-$tmp[2]";
                if ($#{$program{$key}} < 0) {
                        push(@order, $key);
                }

                push(@{$program{$key}}, [@tmp]);
        }

        $dbh->disconnect();

        print "
    = 29); print ">\n"; printf "

    %d件の一致、キーワード: %s

    < br />\n", $#order+1, $query; foreach my $k (@order) { my $i = 0; foreach my $a (sort {$a->[8] <=> $b->[8]} @{$program{$k}}) { my ($dc_name, $dc_ch, $dv_start_time, $end_time, $dv_duration, $dv_name, $dv_favorite, $dp_offset, $dp_offset_sec, $dp_text) = @$a; my $t = Time::Piece->strptime($dv_start_time, "%F %T"); my $start_time = $t->strftime("%y%m%d-%H%M%S"); my $link = join("-", ($start_time, $end_time, $dc_ch)); if ($i++ == 0) { print "<li"; print " data-icon='star'" if ($dv_favorite); print ">"; print "

    $dv_name

    "; print "

    $dc_name $dv_start_time ($dv_duration)

    \n"; } print "

    $dp_offset\t$dp_text

    \n"; } print "
    \n"; } print "
\n"; }
特に変わった処理はしていませんが、お気に入りフラグ(dv_duration)が立っている番組は、アイコンを☆マークに変えています。(以前に書いたかもしれませんが、jQuery Mobileを利用しています。)
また、お気に入りフラグが立っている番組は、ディスクの空き容量確保のためのデータ自動削除処理で、削除対象から除外されるようにしています。

<pre>タグ内で、<br />がうまく記述できないので、表記が少しおかしくなっています。

2012/5/1追記)
Internet Explorerで表示が崩れていたので修正しました。
<pre>~</pre>内でも、HTMLタグを解析するような動作をしているようなので、一部<を&lt;としました。

2012年4月21日土曜日

字幕検索 その2

字幕検索のつづき、データの入れ込み部分についてです。

字幕データの取り出しは、ぱぱネット(仮)さんのdumpeit最新版20110718を使わせてもらうことにしました。 TSの切り出しやEPGの取出しができたりと高機能なのでreq.plやepg.plを置き換えるのがスマートな気がしますが、既にts.plやepg.plを弄っちゃってるので、字幕データの取り出し機能だけ利用することにしました。

次のようなオプションでdumpeitを実行すると、拡張子が.subの字幕ファイルが生成されるので、その内容をDBに取り込むことにします。
dumpeit -f TSファイル
字幕ファイルからDBへのデータ取り込みは、次のように処理しています。
my $sql = make_sql('/path/to/xxxx.sub');
if (defined $sql) {
        store_data($sql);
}

sub make_sql()
{
        my ($file) = shift @_;

        if (!-e $file or -z $file) {
                return undef;
        }

        if ($file !~ /(\d{8})_Ch(\d+)\.sub$/) {
                return undef;
        }
        my $dt = $1;
        my $ch = $2;

        my $t = Time::Piece->strptime($dt, "%y%m%d%H");
        my $date = $t->ymd;


        my @sql = ();
        open my $fh, "<", $file or do {
                return undef;
        };

        my ($t1, $t2, $dv_id) = (undef, undef, undef);
        while (my $line = <$fh>) {
                chomp($line);
                my ($tmp, $msg) = split(/\s/, $line, 2);
                my ($tm, $pid_s) = split(/\./, $tmp);

                my $t = Time::Piece->strptime("$date $tm", "%F %T");

                if (!defined $dv_id or !($t1 <= $t and $t <= $t2)) {
                        my ($id, $stime, $etime) = find_event($ch, "$date $tm");
                        if (defined $id) {
                                $t1 = Time::Piece->strptime($stime, "%F %T");
                                $t2 = Time::Piece->strptime($etime, "%F %T");
                        }
                        $dv_id = $id;
                }

                push @sql, sprintf "($ch, '$date $tm', $pid_s, '$msg', %s)", $dv_id ? $dv_id : "NULL";
        }
        close($fh);

        if ($#sql < 0) {
                return undef;
        }

        my $sql_header = 'INSERT INTO da_caption ';
        $sql_header .= '(dc_ch, dp_start_time, dp_pid, dp_text, dv_id) VALUES ';

        return $sql_header . join(',', @sql);
}
データをそのまま入れているだけですが、検索結果を表示するときに番組情報を調べやすくするために、次の処理で番組データとの関連を追加しています。
sub find_event
{
        my ($ch, $date) = @_;

my $sql = <<END;
select
        e.dv_id,
        e.dv_start_time,
        addtime(e.dv_start_time, e.dv_duration)
from
        da_event as e
        inner join
                da_channel as c
        on
                e.dv_service_id = c.dc_service_id
where
        e.dv_start_time <= ?
and
        ? < addtime(e.dv_start_time, e.dv_duration)
and
        c.dc_ch = ?
order by
        e.dv_start_time
limit
        1
END

        my $dbh = dbiConnect();

        my ($sth, $rv) = ();
        eval {
                $sth = $dbh->prepare($sql);
                $rv = $sth->execute($date, $date, $ch);
        };

        return undef if ($@);
        my @events = ();
        while (my $ref = $sth->fetch()) {
                my ($dv_id, $start_time, $end_time) = @$ref;
                push(@events, [$dv_id, $start_time, $end_time]);
        }

        dbiDisconnect($dbh);

        if ($#events < 0) {
                return undef;
        }

        return @{$events[0]};
}
コミット処理は、次のようにしています。
字幕データ用のテーブルは、MyISAMなので意味が無い気がしますが、その他のテーブルはInnoDBで利用しているので処理を共通化しています。 
sub store_data
{
        my ($sql) = shift @_;

        my $dbh = dbiConnect();

        my $retry=3;
        while ($retry--) {
                eval {
                        my $ddh = $dbh->prepare($sql);
                        my $rv = $ddh->execute();

                        $dbh->commit();
                };
                last if (!$@);
                my $rnd = int(rand(10)) +1;
                my $st = 100000 * $rnd;
                printf STDERR "sleep %.1fs, retry(%d)", $st/1000000, $retry;
                usleep($st);
        }
        if ($retry < 0) {
                print STDERR "store-caption was give up.";
        }

        dbiDisconnect($dbh);

        return !$@;
}
あまり考えないでコーディングを始めてしまうので、テーブル構成がイマイチとか後から思ったりしたのですが…遊びなので結局そのままです。

こんな感じでスクリプト一つ作り、一時間毎にTSデータから字幕データをDBに取り込んで、約24日分の番組情報のデータ量は以下のようになっています。 
mysql> select count(*) from da_caption;
+----------+
| count(*) |
+----------+
|  1195794 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from da_event;
+----------+
| count(*) |
+----------+
|     8436 |
+----------+
1 row in set (0.00 sec)

mysql>
この状態で字幕検索を行っても、今のところ一秒以内に結果が返ってくるので、CGIからの読み出しでもストレス無く使えています。

2012年4月14日土曜日

字幕検索 その1

EPGデータから番組名と番組情報の検索は出来るようにして使っていましたが、少し物足りなくなってきたので、字幕データも突っ込んで全文検索できるようにしてみました。
# MySQLで全文検索を試してみたかったんです。

CentOS 6を利用しているので、MySQLは下記のバージョンを使用しています。

% rpm -q "mysql-*"
mysql-5.1.61-1.el6_2.1.x86_64

MySQLで日本語の全文検索をするために、MySQL full-text parser plugin collectionmecab pluginを使ってみることにしました。
上記のソフトを付属のINSTALLファイルを参照して、インストールします。
ロケールにUTF8を使用しているので、mecab-0.993, mecab-ipadic-2.7.0-20070801のconfigureのオプションに --with-charset=utf8 を指定する必要がありました。(デフォルトはEUC)

取りあえず、こんな感じでテーブルを作ると、
create table da_caption (
        dp_id integer primary key NOT NULL AUTO_INCREMENT,
        dc_ch integer,
        dp_start_time timestamp,
        dp_pid integer,
        dv_id integer,
        dp_text text
        ) engine = myisam DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

create index da_caption_index on da_caption (dp_start_time, dc_ch);
create fulltext index da_caption_fulltext_index on da_caption (dp_text) with parser mecab;
次のような検索できるようになります。
mysql> select * from da_caption where match(dp_text) against('バイク -自転車' in boolean mode) limit 5;
+---------+-------+---------------------+--------+--------+--------------------------------------------------------------------------------------------+
| dp_id   | dc_ch | dp_start_time       | dp_pid | dv_id  | dp_text                                                                                    |
+---------+-------+---------------------+--------+--------+--------------------------------------------------------------------------------------------+
| 2337207 |    27 | 2012-03-31 16:19:30 |   1415 | 190710 | 同じ排気量のバイクに乗るため→                                                              |
| 2337241 |    27 | 2012-03-31 16:22:40 |   1415 | 190710 | でも ほとんど バイクのパーツ代に消えてしまうんだそうです。                               |
| 2399738 |    23 | 2012-04-01 19:54:08 |   4231 | 194390 | 知人らしきバイクの女性が颯爽と登場。                                                       |
| 2380118 |    21 | 2012-04-01 13:01:38 |    340 | 193580 | <ベトナムは バイク大国>                                                                 |
| 2380122 |    21 | 2012-04-01 13:02:03 |    340 | 193580 | <バイクの多い…>                                                                         |
+---------+-------+---------------------+--------+--------+--------------------------------------------------------------------------------------------+
5 rows in set (0.00 sec)

2012年4月12日木曜日

USBポートの電源制御

ワンセグ野郎を始めて2ヶ月位経ちますが、3回ほどdvbstreamからデータを取れなくなって停止していることがありました。
dvbstream起動時に、次のようなメッセージを出力しているので、信号を受信できない状態になるようです。

Bit error rate: 0
Signal strength: 0
SNR: 0
FE_STATUS:
Setting filter for PID 8192
FILTER 8192: DMX SET PES FILTER: Connection timed out

こうなってしまうとUSBデバイスを抜き差ししないと回復しません。
USBデバイスへの電源On/Offでよさそうなのですが、リブートしてもUSBデバイスへの給電は切れないようなので、USBポートの電源制御をする必要があるようです。

USBコントローラーが電源制御をサポートしていれば、ソフト的に制御できることが分かったのですが、残念ながら使用しているマザーボードでは未サポートでした。

さらに調べるとポート毎に電源制御ができるUSB HUBがあり、Linux向けのソフト(節電USB・HUB(U2H-SW4)の制御ソフト)を公開している方を見つけました。
作者の方は、sianoチップ利用のワンセグ野郎ユーザーのようで、同じ症状改善のために書かれたようです。すばらしい。

早速、コンパイルして使用してみましたが、私の環境ではそのままでは上手く動きませんでした。
HUBの仕様変更を疑ってSnoopyProでデータを確認したり、あれこれ調べたところ、/dev/hidrawXとの送受信データの先頭に1byte付けてあげる(レポートID用?)と上手く動くことが分かりました。

で、ts.plに組み込みたかったので、perlで書いてみました。

set_status('/dev/hidraw0', 1, 0);  # hidraw0のポート1の電源OFF
usleep((1000000*0.3));
set_status('/dev/hidraw0', 1, 1);  # hidraw0のポート1の電源ON

sub set_status()
{
        my ($file, $port_num, $power) = @_;

        sysopen my $fh, $file, O_RDWR|O_NONBLOCK or do {
                print STDERR "cannot open $file: $!\n";
                return 0;
        };

        my @set_status = (0,0x03,0x5d,0,0,0,0,0,0);

        if ($port_num == 1) {
                $set_status[4] = 0x05;
        }
        elsif (1 < $port_num and $port_num < 5) {
                $set_status[4] = int($port_num);
        }
        else {
                return 0;
        }

        $set_status[5] = 0x01 if ($power);

        my $dat = pack("C9", @set_status);
        my $r = syswrite $fh, $dat, 9;

        if ($r != 9) {
                print STDERR "syswrite failed: f=$file, p=$port_num: $!\n";
                return 0;
        }
        close $fh;
}

U2H-SW4は、lsusbで確認するとUSB portを5つ持っていて、USB port1がHIDになっているようです。
USB port5が機器に印刷されている1番ポートに対応しています。

ポートの状態確認は、次のようにして取得できます。
4byte目の該当するビットが立っている場合は、電源Onを表しています。

my $port = get_status("/dev/hidraw0");
print "$file\n";
foreach my $i (0..3) {
        my $on = $port & (1<< $i);
        printf " port%d: %s\n", $i+1, $on ? "On" : "Off";
}

sub get_status()
{
        my ($file) = shift @_;

        sysopen my $fh, $file, O_RDWR|O_NONBLOCK or do {
                print STDERR "cannot open $file: $!\n";
                return undef;
        }

        my @get_status = (0,0x03,0x5d,0x02,0,0,0,0,0);

        my $dat = pack("C9", @get_status);
        my $r = syswrite $fh, $dat, 9;

        my ($rout, $info) = (undef, undef);
        my $rin = '';
        vec($rin, fileno($fh), 1) = 1;
        my ($nfound, $timeleft) = select($rout=$rin, undef, undef, 3);

        $r = read $fh, $info, 9;
        close $fh;

        return undef if ($r != 9);

        my @st = unpack("C9", $info);
        if (($st[3] & 0xc3) != 0x03 and $st[5] == 0x75) {
                printf STDERR "unknown data received: %s\n", unpack("H*", $info);
                return undef;
        }

        my ($port, $i) = (0, 0);
        foreach my $flg (0x20, 0x04, 0x08, 0x10) {
                $port |= (1 << $i) if ($st[3] & $flg);
                $i++;
        }
        return $port;
}
ts.plに組み込むにあたり、dvbstreamで使用しているDVBデバイスが接続しているU2H-SW4を見つける必要があります。
もっとスマートな方法があると思いますが、udevadmコマンドでdevpathを調べることで接続しているU2H-SW4とHUB portを特定できました。

U2H-SW4のHIDは、次のように/dev/hidraw*からベンダー/プロダクトコードが一致するものを調べ、udevadmコマンドでdevpathを取得しておきます。
説明用のコードでは、見やすさのためにエラー処理はdie()にしました。
sub find_u2hsw4()
{
        my $HIDIOCGRAWINFO = 0x80084803;

        opendir my $dh, '/dev' or die "cannot opendir /dev: $!";
        my @files = sort grep /^hidraw\d+$/, readdir($dh);
        closedir $dh;

        my @target = ();
        foreach my $f (@files) {
                my $file = "/dev/$f";
                my $info = undef;

                open my $fd, '+<', $file or die "cannot open $file: $!";
                ioctl($fd, $HIDIOCGRAWINFO, $info) or die "ioctl failed: $file: $!";
                close $fd;

                my ($busy, $vendor, $product) = unpack("Iss",$info);

                # find ActionStar USB HID
                if ($vendor == 0x2101 and ($product & 0xffff) == 0x8501) {
                        my $path = `/sbin/udevadm info --query=path --name=$file`;
                        chomp($path);
                        $path =~ s!/hidraw/${f}$!!;
                        push @target, [$file, $path];
                }
        }

        return @target;
}
上記で見つけたHIDのdevpathと、dvbstreamで使用している/dev/dvb/adapterX/frontendXのdevpathを調べることにより、接続しているHUBとHUB portを見つけることが出来ます。

ts.plでdvbstreamにSIGTERMを送った後、dvbstreamのログから直近でSNR: 0を見つけた場合に、電源をOn/Offするようにして使ってます。

この記事からSyntaxHighlighter を設定してみました。