Walrus,Digit. | 一覧 | 検索 | 更新履歴(RSS) | 新規作成
はてなブックマークに追加 はてなブックマークを表示 編集 | 編集(管理者用) | 差分

Perlスクリプト/feed2growl.pl

編集

新着情報をgrowlに送る、Windows用の簡単なフィード(RSS、ATOMフィード)クローラ。

feed2growlとは

編集

Windows用の簡単なフィード(RSS、ATOMフィード)クローラです。新着情報があればGrowlに送ります。

以下のような特徴をもっています。

feed2growlは「Windows上でフィードの更新をポップアップ通知させたい」という個人的なニーズで書かれました。このために、Windowsのタスクに登録でき、GUIやコマンドプロンプトなしで動作し、新着情報はGrowl(やそのクローンであるGrowl for WindowsWhineに通知させたいと思いました。コマンドプロンプトを出させないためには、PAR::PackerでGUIを持たないGUIアプリケーションとしてexeファイル化する方法が簡単でした。ただしexeファイル化する以上、設定情報はすべて外出しにする必要がありました。タスクマネージャに登録して以降は動作を意識したくない、と思ったら、プロキシ情報はInternet Explorerから取得して来るのが最も便利でした。

こうした理由で、一般的ではない特徴を持ったfeed2growl.pl(100行ちょっとのスクリプト)を書きました。

ファイル

zipファイルには以下が入っています。

ファイル名内容
feed2growl.plPerlスクリプト本体です。
config.yml.sample設定ファイルのサンプルです。config.ymlなどにコピーして使います。
feed2growl.exeexe化されたfeed2growlです。

exeファイルは、以下のようにすれば実行できます。

feed2growl

Perlスクリプトは、以下のようにすれば実行できます。Perlスクリプトの実行時には、モジュールを追加インストールする必要があるかもしれません。

perl feed2growl.pl

上記のように実行すると、カレントディレクトリのconfig.ymlを設定ファイルとして読み込みます。他のファイルを読み込む時は、-cオプションで指定します。

feed2growl -c myconfig.yml

Windowsのタスクに登録する時は、コマンドプロンプトのような画面が出ないexeファイルを使うと良いと思います。タスクスケジューラを使い、実行ディレクトリにfeed2growl.exeがあるディレクトリ、実行コマンドを上記のようにすればよいでしょう。

スクリプトを変更し、exeファイルを作り直すには、以下のようにします。ただし、Perlスクリプトとして実行できる環境、PARでPerlスクリプトをexe化できる環境が整っていることが必要です。

pp --gui feed2growl.pl -o feed2growl.exe

feed2growl.plのソース

use strict;
use warnings;
use Encode;
use LWP::UserAgent::ProxyAny;
use Net::GrowlClient;
use Win32::TieRegistry;
use Win32::Registry;
use XML::FeedPP;
use YAML;

# initialize

my %args = @ARGV;
my $yaml = exists($args{'-c'}) ? $args{'-c'} : 'config.yml';
my $config = load_config($yaml);

my $ua = LWP::UserAgent::ProxyAny->new;
$ua->env_proxy;
if ($ua->proxy('http') && $config->{'proxy_auth'}) {
  foreach my $auth (@{$config->{'proxy_auth'}}) {
    next if ($ua->proxy('http') ne $auth->{'proxy'});
    my $header = $ua->default_headers;
    $header->proxy_authorization_basic($auth->{'username'}, $auth->{'password'});
    $ua->default_headers($header);
    last;
  }
}
$XML::FeedPP::TREEPP_OPTIONS->{'lwp_useragent'} = $ua;

my $growl_opt = $config->{'growl'} || {};
my $growl = Net::GrowlClient->init(%$growl_opt);

# process

foreach my $group (@{$config->{'group'}}) {
  next unless ($group->{'feed'});
  my $title = $group->{'title'};
  my $limit = $group->{'limit'};
  my @items = ();

  my @feeds = @{$group->{'feed'}};
  foreach my $feed (@feeds) {
    my %recent = map { $_ => 1 } @{$feed->{'recent'}};
    my $url = $feed->{'url'} || next;
    my $fpp = eval { XML::FeedPP->new($url) };
    next if ($@);
    my @feed_items = $fpp->get_item();
    @feed_items = sort {$b->pubDate() gt $a->pubDate()} @feed_items;
    @feed_items = splice(@feed_items, 0, $limit) if ($limit);
    my @recents = map { $_->link() } @feed_items;
    $feed->{'recent'} = [ splice(@recents, 0, 5) ];
    $feed->{'title'} = $fpp->title();
    $title = $feed->{'title'} unless ($feed->{'title'});
    foreach my $item (@feed_items) {
      last if ($recent{$item->link()});
      $item->{'feed'} = $feed;
      push(@items, $item);
    }
  }

  @items = sort {$b->pubDate() gt $a->pubDate()} @items;
  @items = splice(@items, 0, $limit) if ($limit);
  foreach my $item (@items) {
    my $message = sprintf("%s\n(%s at %s)", $item->description, $item->pubDate, $item->{'feed'}->{'title'});
    $growl->notify(
      application => $title,
      title       => decode_utf8($item->title),
      message     => decode_utf8($message)
    );
    sleep(1);
  }
}

# finish

YAML::DumpFile($yaml, $config);
exit();

sub load_config {
  my $yaml = shift;
  die sprintf("Config file is not specified.") unless (length($yaml));
  die sprintf("File '%s' is not exists.", $yaml) unless (-f $yaml);

  my $config = YAML::LoadFile($yaml);
  die sprintf("File '%s' is not valid.") unless ($config);

  my @groups = ();
  push(@groups, @{delete($config->{'group'})}) if (ref($config->{'group'}) eq 'ARRAY');
  push(@groups, delete($config->{'group'})) if ($config->{'group'});
  push(@groups, delete($config->{'feed'})) if ($config->{'feed'});
  foreach my $group (@groups) {
    $group = { 'feed' => $group } if (ref($group) ne 'HASH');
    $group->{'title'} = '' unless (defined($group->{'title'}));
    $group->{'limit'} = 0  unless (defined($group->{'limit'}));
    $group->{'feed'} = [$group->{'feed'}] if (ref($group->{'feed'}) ne 'ARRAY');
    foreach my $feed (@{$group->{'feed'}}) {
      $feed = {'url' => $feed} if (not ref($feed));
      $feed->{'title'} = '' unless (defined($feed->{'title'}));
      $feed->{'recent'} = [$feed->{'recent'}] if ($feed->{'recent'} && not ref($feed->{'recent'}));
      $feed->{'recent'} = [] unless (UNIVERSAL::isa($feed->{'recent'}, 'ARRAY'));
    }
    @{$group->{'feed'}} = grep { $_->{'url'} } @{$group->{'feed'}};
  }
  $config->{'group'} = \@groups;

  my @proxy_auths = ();
  push(@proxy_auths, @{delete($config->{'proxy_auth'})}) if (ref($config->{'proxy_auth'}) eq 'ARRAY');
  push(@proxy_auths, delete($config->{'proxy_auth'})) if ($config->{'proxy_auth'});
  if (@proxy_auths) {
    foreach my $proxy_auth (@proxy_auths) {
      $proxy_auth = {'proxy' => $proxy_auth} if (not ref($proxy_auth));
      $proxy_auth->{'username'} = '' unless (defined($proxy_auth->{'username'}));
      $proxy_auth->{'password'} = '' unless (defined($proxy_auth->{'password'}));
    }
    $config->{'proxy_auth'} = \@proxy_auths;
  }

  return $config;
}

config.yml.sampleのソース

---
group:
  - feed:
      - http://tsukamoto.tumblr.com/rss
      - http://walrus.vox.com/library/posts/atom.xml
    limit: 5
    title: Makio Tsukamot Blog & Reblogs
growl:
  CLIENT_TYPE_NOTIFICATION: 1
  CLIENT_PASSWORD: ''
  CLIENT_SKIP_REGISTER: 1
proxy_auth:
  - proxy: http://myproxy.example.com:8080/
    username: alibaba
    password: opensesami