SweetPotato::Plagger このページをアンテナに追加 RSSフィード

2007-07-04

[] CustomFeed::Manganomori修正 18:30  CustomFeed::Manganomori修正 - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::Manganomori修正 - SweetPotato::Plagger  CustomFeed::Manganomori修正 - SweetPotato::Plagger のブックマークコメント

「旬」の表記が「(上|中|下)旬」から単純な「(上|中|下)」に変わった模様。それに対応すると同時に,「(上|中|下)旬」に戻られても困るから正規表現で判断するようにした。

トラックバック - http://plagger.g.hatena.ne.jp/SweetPotato/20070704

2007-07-01

[] CustomFeed::GoogleCalendar  CustomFeed::GoogleCalendar - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::GoogleCalendar - SweetPotato::Plagger  CustomFeed::GoogleCalendar - SweetPotato::Plagger のブックマークコメント

Googleカレンダーに限って言えばAtomを返すAPIを叩く方がより柔軟にデータの取得が出来ると思う。

つーか俺もプラグイン作る前に確認しようぜ。

iCal形式のファイルを取得し,イベントごとにエントリ化したフィードを生成する。

名前が”GoogleCalendar”となっているが,iCal形式であればおそらくGoogle Calendar以外でも取得・生成が可能だと思う。ただし確認はしていない。

Plagger/Plugin/CustomFeed/GoogleCalendar.pm

package Plagger::Plugin::CustomFeed::GoogleCalendar;
use utf8;
use strict;
use base qw( Plagger::Plugin );

use Data::ICal;
use DateTime::Format::ICal;
use URI;

use Plagger::Date;
use Plagger::Util;

sub register {
    my ($self, $context) = @_;
    $context->register_hook(
        $self,
        'subscription.load' => \&load,
    );
}

sub load {
    my ($self, $context) = @_;

    my $calendars = $self->conf->{calendar} or return;
       $calendars = [$calendars] unless ref $calendars;

    for my $calendar (@$calendars) {
        my $now = $calendar->{now}
                ? Plagger::Date->parse_dwim(delete $calendar->{now})
                : Plagger::Date->now;
        my $feed = Plagger::Feed->new;
           $feed->aggregator( sub { $self->aggregate($context, $calendar, $now); });
        $context->subscription->add($feed);
    }
}

sub aggregate {
    my ($self, $context, $config, $now) = @_;

    my $content = Plagger::Util::load_uri(URI->new($config->{url})) or return;

    my $calendar = Data::ICal->new(data => $content);
    unless ($calendar) {
        $context->error($calendar->error_message);
        return;
    }

    my $feed = Plagger::Feed->new;
       $feed->url($config->{url});
       $feed->title($config->{title} || $self->unescape($calendar->property('x-wr-calname')->[0]->value));
       $feed->description($config->{desc} || $self->unescape($calendar->property('x-wr-caldesc')->[0]->value));

    my $events = $calendar->entries;
    my $timezone = shift @$events;

    for my $event (@$events) {
        my $data;
        for my $key (qw/ dtstart dtend uid summary description location /) {
            $data->{$key} = $event->property($key);
            $data->{$key} = $data->{$key}->[0]->value if $data->{$key}; # @ADHOC
        }

        my $allday = ($data->{dtstart} =~ /^\d{8}$/) && ($data->{dtend} =~ /^\d{8}$/);
        for my $key (qw/ dtstart dtend /) {
            $data->{$key} = DateTime::Format::ICal->parse_datetime($data->{$key});
        }

        my $entry = Plagger::Entry->new;
           $entry->id($self->unescape($data->{uid}));
           $entry->title($self->unescape($data->{summary}));
           $entry->body($self->unescape($data->{description}));
           $entry->author($self->unescape($data->{location}));

        if (Plagger::Date->compare($now, $data->{dtstart}) < 0) {
            $entry->date($data->{dtstart});
            $entry->add_tag('prep');
        } else {
            $entry->date($allday ? $data->{dtend}->clone->subtract(days => 1) : $data->{dtend});
            if (Plagger::Date->compare($now, $data->{dtend}) < 0) {
                $entry->add_tag('open');
            } else {
                $entry->add_tag('close');
            }
        }

        $feed->add_entry($entry);
    }

    $context->update->add($feed);
}

sub unescape {
    my ($self, $content) = @_;

    # to single line
    $content = join '', split /\r\n[ ]/, $content;

    my @unescaped = ();
    my $state = 0;
    for my $char (split //, $content) {
        if ($state == 1) {
            push @unescaped, ($char eq 'n' ? "\n" : $char);
            $state = 0;
        } elsif ($char eq "\\") {
            $state = 1;
        } else {
            push @unescaped, $char;
        }
    }
    join '', @unescaped;
}

1;

エントリのauthorには,iCalで言うVEVENTのLOCATIONがセットされる。

開催前・開催中・開催終了のイベントのエントリには,それぞれprep,open,closeというタグが付与される。

config.calendar.yaml

この例では,取得した全てのイベントの中から,今週のイベントのみを残し,日付順に整列して,RSS形式で出力する。

plugins:
  - module: CustomFeed::GoogleCalendar
    config:
      calendar:
        - url: http://www.google.com/calendar/ical/hb3ceqibm3d7kbp99e61klnvpo%40group.calendar.google.com/public/basic.ics

  - module: Filter::Rule
    rule:
      - module: Expression
        expression: |
          use Plagger::Date;
          Plagger::Date->compare(Plagger::Date->today, $args->{entry}->date) <= 0
            &&
          Plagger::Date->compare($args->{entry}->date, Plagger::Date->today->add(days => 7)) < 0;

  - module: Filter::SortEntries
    config:
      expression: |
        use Plagger::Date;
        Plagger::Date->compare($a->date, $b->date);

  - module: Publish::Feed
    config:
      format: RSS
      dir: ./calendar
      filename: calendar.xml
      taguri_base: sweetpotato

Subscription::Configと同様,複数のカレンダーをconfigファイルに登録できる。その場合はひとつのカレンダーがひとつのフィードになる。

各カレンダーにはまた,上で述べた開催前・開催中・開催終了の「基準」となる日時を指定することもできる。指定しない場合は現在の日時が基準になる。

Filter::SortEntriesについては以下の記事を参照。

何ができるの?

例えば,カレンダーから今週のイベントのみをふるい分けて,フィードに出力して,そのフィードをJavaScriptで読み込んでブログの柱に表示する,とか。イベントの管理がカレンダーで一元管理できるようになるわけだ。

他には,カレンダーから今日の予定のみをふるい分けて,自分のメールアドレスに毎朝送るとか。リマインダー的な使い方。Google Calendarデフォルトのリマインダー機能よりもリッチな処理ができるはず。

でもPlaggerだからきっともっといろいろできるはずだよ!

補足

プラグイン名をCustomFeed::iCalにしなかった理由は,半分はGoogle Calendar以外のiCalでもちゃんとテストをすることが面倒だったことから。

もう半分は,iCalサポートは次のバージョンのPlaggerで既に予定されており,おそらくそうなるであろう名前と衝突するのを避けたかったから。これについてはmiyagawa氏の第9回XML開発者の日のスライド186枚目に:

Calendar Support

iCal parser & emitter

hCalendar microformats

.ics attached in emails

Sync::SyncML

Plagger - スポーツ:マイライフ

と書かれている。

nyarla-netnyarla-net2007/07/01 10:27自分も同じ名前で似たようなプラグイン作ってます。中身が微妙に違いますが。
http://nyarla.net/blog/plagger24

SweetPotatoSweetPotato2007/07/01 10:47しまった!車輪の再生産になっちゃうかもしれない!
nyarla-netさんは比較にcmpを使ってらっしゃいますが,これは日付(date)での整列も可能ですか?

nyarla-netnyarla-net2007/07/01 12:57多分できないかと。
僕の作ったプラグインの場合、指定されたプロパティを単純に比較しているだけなので、そのあたり込み入ったことが出来ないと思います。

まあSweetPotatoさんのか僕のかどちらかをベースにしてくっつけてしまえば良いと思いますが。

トラックバック - http://plagger.g.hatena.ne.jp/SweetPotato/20070701

2007-04-22

[] CustomFeed::MelonBooks修正 18:16  CustomFeed::MelonBooks修正 - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::MelonBooks修正 - SweetPotato::Plagger  CustomFeed::MelonBooks修正 - SweetPotato::Plagger のブックマークコメント

リファクタリングを行った。機能には変更なし。ソースは上記リンクから。

トラックバック - http://plagger.g.hatena.ne.jp/SweetPotato/20070422

2007-04-11

[] CustomFeed::FeedAsEntry  CustomFeed::FeedAsEntry - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::FeedAsEntry - SweetPotato::Plagger  CustomFeed::FeedAsEntry - SweetPotato::Plagger のブックマークコメント

各フィードに,そのフィードと同じURLとタイトルを持つエントリを追加する。

Plagger/Plugin/CustomFeed/FeedAsEntry.pm

package Plagger::Plugin::CustomFeed::FeedAsEntry;
use strict;
use base qw( Plagger::Plugin );

sub register {
    my ($self, $context) = @_;
    $context->register_hook(
        $self,
        'customfeed.handle' => \&aggregate,
    );
}

sub aggregate {
    my ($self, $context, $args) = @_;
    my $entry = Plagger::Entry->new;
    $entry->link($args->{feed}->url);
    $entry->title($args->{feed}->title);
    $args->{feed}->add_entry($entry);
    $context->update->add($args->{feed});
    return 1;
}

1;

経緯

Subscription::WWWC + EFT + Filter::Diffで似非はてなアンテナ作れそうじゃん」と思ってとりかかったものの,RSSやAtomを持たないページが途中で「~ is not aggregated by any aggregator」というメッセージとともにごっそりと削除される壁にぶちあたった。

Plaggerのソースを見たら,customfeed.handleフェーズの段階でどのモジュールにも処理されずエントリが生成されなかったフィードは削除されてしまうことが分かった。RSSやAtomを持つページはデフォルトでAggregator::Simpleが処理してくれたけど,そうでない普通のページは削除されてしまっていたわけだ。

「じゃあcustomfeed.handleフェーズで無理矢理エントリを追加しなきゃならんのか」ということで作られたのがこのプラグイン。他に使い道は……あまりなさそうだなあ。

トラックバック - http://plagger.g.hatena.ne.jp/SweetPotato/20070411

2007-03-03

[] CustomFeed::Manganomori修正 02:57  CustomFeed::Manganomori修正 - SweetPotato::Plagger を含むブックマーク はてなブックマーク -  CustomFeed::Manganomori修正 - SweetPotato::Plagger  CustomFeed::Manganomori修正 - SweetPotato::Plagger のブックマークコメント

リファクタリングを行い,さらにコードの無駄を省きました。

また,フィードタイトルには単行本タイトルのみを,フィード本文には作者,出版社,価格をカンマで区切ったものを指定するようにしました。

これが気に入らない方は,各自,50~53行目の,

$entry->title($item->{title});
$entry->author($item->{author});
$entry->tags([$item->{publisher}]);
$entry->body("$item->{author}, $item->{publisher}, $item->{price}");

の部分の引数を適当に変えてカスタマイズして下さい。

使えるキーの値は,80~83行目の,

push @$list, {
    day => $1, title => $2, author => $3, price => $4,
    publisher => $publisher,
}

のものが全てです。

トラックバック - http://plagger.g.hatena.ne.jp/SweetPotato/20070303