rss-1.0/0000755000000100000010000000000011043341143010273 5ustar phtphtrss-1.0/dbsetup.pl0000755000000100000010000000167011043265426012316 0ustar phtpht#!/usr/bin/perl use strict; use warnings; use SQLite::DB; use Data::Dumper; my $sql = SQLite::DB->new('rss.sqlite'); $sql->connect() or die $sql->get_error; $sql->transaction_mode(); my $create = 'create table articles ('; for my $col (qw/feed title link author description/) { $create .= "$col text not null, "; } $create .= 'time integer not null, read integer not null default 0,' . 'id integer not null primary key autoincrement)'; $sql->exec($create) or die $sql->get_error; $sql->exec('create index feed on articles (feed)'); $sql->exec('create index read on articles (feed, read)'); $sql->exec('create index time on articles (time desc)'); $sql->exec( 'create unique index article on articles (feed, link)'); $sql->select('SELECT * FROM sqlite_master', sub { my $st = shift; while (my $r = $st->fetchrow_hashref) { print Dumper($r); } } ) or die $sql->get_error; $sql->commit() or die $sql->get_error; $sql->disconnect(); rss-1.0/feeds.yaml0000644000000100000010000000036611043341136012254 0ustar phtpht--- root: root.clanky: http://rss.root.cz/2/clanky/ root.zpravicky: http://rss.root.cz/2/zpravicky/ root.diskuse: http://www.root.cz/diskuse/ root.wiki: http://wiki.root.cz/Site/AllRecentChanges?action=rss root.blog: http://blog.root.cz/feed/rss rss-1.0/fetch.pl0000755000000100000010000000523011043337201011724 0ustar phtpht#!/usr/bin/perl use strict; use warnings; use XML::RSS; use XML::Entities; use LWP::UserAgent; use SQLite::DB; use DateTime::Format::Mail; use DateTime::Format::ISO8601; use YAML; # start database connection... my $sql = SQLite::DB->new('rss.sqlite'); $sql->connect() or die $sql->get_error; $sql->transaction_mode(); # initialize objects... my $agent = LWP::UserAgent->new(env_proxy => 1); my $feeds = YAML::LoadFile('feeds.yaml'); # fetch one feed and store it to database... sub fetch_one { my ($feed, $url) = @_; # retrieve data from internet... my $reply = $agent->get($url); if (not $reply->is_success) { warn($feed . ':' . $reply->status_line); return; } # parse RSS... my $rss = XML::RSS->new(); eval { $rss->parse($reply->content); }; if ($@) { warn($feed . ':' . $@); return; } my $insert = <<'_END'; insert into articles (feed, title, link, author, description, time) values (?, ?, ?, ?, ?, ?) _END my $update = <<'_END'; update articles set feed=?, title=?, link=?, author=?, description=?, time=? where id=? _END # process all items... for my $itemref (@{$rss->{items}}) { my %item = %$itemref; my $hasdc = ref $item{dc} eq 'HASH'; # figure missing 'author' from dc namespace... if (!$item{author} and $hasdc) { my %dc = %{$item{dc}}; $item{author} = $dc{creator} ? $dc{creator} : $dc{contributor}; } # make sure all items are defined... for my $i (qw/title link author description pubDate/) { $item{$i} ||= q//; $item{$i} = XML::Entities::decode('all', $item{$i}); } my $datetime; eval { # try to get datetime from pubDate, from dc, # or from 'now'... if ($item{pubDate}) { $datetime = DateTime::Format::Mail->parse_datetime( $item{pubDate}); } elsif ($hasdc) { $datetime = DateTime::Format::ISO8601->parse_datetime( $item{dc}->{date}); } else { $datetime = DateTime->now; } }; if ($@) { warn($feed . ':' . $@); $datetime = DateTime->now; } # see if db record already exists... my $r = $sql->select_one_row( 'select id from articles where feed=? and link=?', $feed, $item{'link'} ); # run update query... $sql->exec( ($r->{id} ? $update : $insert), $feed, @item{qw/title link author description/}, $datetime->epoch, ($r->{id} ? $r->{id} : ()) ) or warn $sql->get_error; } } # process all feeds... for my $feed (sort keys %$feeds) { next if !$feeds->{$feed}; fetch_one($feed, $feeds->{$feed}); } # finalize database connection... $sql->exec('analyze') or die $sql->get_error; $sql->commit() or die $sql->get_error; $sql->disconnect(); # notify GUI of changes... system('killall -USR1 rss.pl 2>/dev/null'); rss-1.0/rss.pl0000755000000100000010000002213711043341106011446 0ustar phtpht#!/usr/bin/perl use strict; use warnings; use Tk; use Tk::Tree; use Tk::ResizeButton; use Tk::ItemStyle; use Tk::ROText; use Readonly; use YAML; use SQLite::DB; use Encode; use DateTime; use POSIX; #use Time::HiRes qw/gettimeofday tv_interval/; my $sql = SQLite::DB->new('rss.sqlite'); $sql->connect() or die $sql->get_error; my ($mw, $feeds, $articles, $one_article); my (%styles, %feed_counts, @sorted_feeds); my $current_feed = q//; my $current_article = -1; my $articles_shown = 0; Readonly my $article_limit => 100; # initialize all widgets sub setup_widgets { Readonly my @popts => qw/-fill both -expand 1/; Readonly my @hopts => qw/-header 1 -scrollbars ose/; # create main window and panes... $mw = Tk::MainWindow->new(-title => 'RSS'); my $pw = $mw->Panedwindow(-orient => 'horizontal')->pack(@popts); my $leftp = $pw->Frame; my $rightp = $pw->Frame; my $rightpw = $rightp->Panedwindow( -orient => 'vertical')->pack(@popts); my $rightupp = $rightpw->Frame; my $rightdnp = $rightpw->Frame; # create items in panes... $feeds = $leftp->Scrolled('Tree', -columns => 3, @hopts )->pack(@popts); $articles = $rightupp->Scrolled('HList', -columns => 4, @hopts )->pack(@popts); $one_article = $rightdnp->ROText(-wrap => 'word')->pack(@popts); $pw->add($leftp, $rightp); $rightpw->add($rightupp, $rightdnp); # helper function for headers... sub do_headers { my ($widgetref, @heads) = @_; my $cnt = 0; # walk through all headers, inserting a ResizeButton... for my $head (@heads) { $widgetref->header('create', $cnt, -itemtype => 'window', -widget => $widgetref->ResizeButton( -text => $head, -widget => \$widgetref, -relief => 'flat', -pady => 0, -column => $cnt, ), ); $cnt++; } } # create headers for lists... do_headers($feeds, qw/feed total unread/); do_headers($articles, qw/title feed author time/); # create common styles... $styles{right} = $feeds->ItemStyle( 'text', -justify => 'right', ); $styles{unread} = $articles->ItemStyle( 'text', -foreground => 'blue', ); $styles{normal} = $articles->ItemStyle('text'); # bind actions to user clicks... $articles->configure( -browsecmd => \&article_changed, -command => \&run_article, ); $feeds->configure(-browsecmd => \&feed_changed); # replace the Tab key binding... $mw->bind('all', '' => undef); $articles->bind('' => sub { $feeds->focus }); $feeds->bind('' => sub { $articles->focus }); } # flush article description... sub clear_one_article { $one_article->delete('1.0', 'end'); } # load article description from db... sub load_one_article { my $r = $sql->select_one_row( 'select description from articles where id=?', $current_article ) or warn $sql->get_error; my $text = decode('utf8', $r->{description}); $one_article->insert('end', $text); } # event handler for browse on articles list sub article_changed { my $article = shift; # handle the 'more' item... if ($article eq 'more') { load_articles(); return; } return if $current_article == $article; $current_article = $article; clear_one_article(); load_one_article(); } # mark current article as read sub mark_current_read { my $r = $sql->select_one_row( 'select read from articles where id=?', $current_article ) or warn $sql->get_error; # mark only if is not already read... if ($r->{'read'} == 0) { # update in database... $sql->exec( q/update articles set read='1' where id=?/, $current_article ) or warn $sql->get_error; # update style on screen... $articles->entryconfigure($current_article, -style => $styles{normal}); for my $col (1..3) { $articles->itemConfigure($current_article, $col, -style => $styles{normal}); } # update counts... --($feed_counts{$current_feed}->[1]); update_count($current_feed, 0, -1); display_counts(); } } # launch article in external browser sub run_article { # shouldn't happen, but you never know... return if $current_article eq 'more'; # mark it as read... mark_current_read(); # fetch link from db... my $r = $sql->select_one_row( 'select link from articles where id=?', $current_article ) or warn $sql->get_error; # fork and exec browser... my $pid = fork(); if (not defined $pid) { warn "fork: $!"; } elsif ($pid == 0) { # need CORE:: to override SQLite's exec CORE::exec( '/opt/seamonkey/bin/seamonkey', '-remote', 'openURL(' . $r->{'link'} . ')' ); die "exec browser: $!"; } } # flush article list... sub clear_articles { $articles->delete('all'); $articles_shown = 0; } # fetch $article_limit more articles from db sub load_articles { # see if all articles already shown... return if $articles_shown >= $feed_counts{$current_feed}->[0]; my $first = undef; # subroutine to process SQL query my $process = sub { my $st = shift; # process all rows... while (my $row = $st->fetchrow_hashref) { my @opts = ( -itemtype => 'text', -style => $row->{'read'} ? $styles{normal} : $styles{unread} ); my $id = $row->{id}; # save first inserted row id... $first = $id unless defined $first; # add widgets... $articles->add($id, -text => decode('utf8', $row->{title}), @opts ); $articles->itemCreate($id, 1, -text => $row->{feed}, @opts ); $articles->itemCreate($id, 2, -text => decode('utf8', $row->{author}), @opts ); my $time = DateTime->from_epoch(epoch => $row->{'time'}); $articles->itemCreate($id, 3, -text => $time->ymd('-') . q/ / x 2 . $time->hms(':'), @opts ); } }; # delete the 'more' entry... $articles->delete('entry', 'more') if $articles->info('exists', 'more'); # fetch and process articles from db... # my $start = [ gettimeofday() ]; $sql->select( 'select id, title, feed, author, time, read from articles' . ' where feed like ? order by time desc' . " limit $article_limit offset $articles_shown", $process, $current_feed . '%', ) or warn $sql->get_error; # warn tv_interval($start); # conveniently make the first inserted row seen and selected... $articles->see($first); $articles->anchorSet($first); article_changed($first); # update count and see if there's need for 'more'... $articles_shown += $article_limit; if ($articles_shown < $feed_counts{$current_feed}->[0]) { $articles->add('more', -text => '[ more ]'); } } # event handler for browse on feeds list sub feed_changed { my $feed = shift; return if $feed eq $current_feed; $current_feed = $feed; clear_articles(); load_articles(); } # refresh counts widgets to match %feeds_counts... sub display_counts { for my $feed (@sorted_feeds) { for my $i (0, 1) { $feeds->itemCreate( $feed, $i + 1, -itemtype => 'text', -style => $styles{right}, -text => $feed_counts{$feed}->[$i], ); } } } # adjust feed's parent groups by specified amount... sub update_count { my ($feed, @delta_counts) = @_; while ($feed =~ s/\.[^.]+$//) { for my $i (0, 1) { $feed_counts{$feed}->[$i] += $delta_counts[$i]; } } } # fetch feeds from YAML and db sub load_feeds { # fetch list from YAML... my $feed_list = YAML::LoadFile('feeds.yaml'); # clear widgets and initialize... $feeds->delete('all'); %feed_counts = (); # due to groups it's most useful to have the keys sorted @sorted_feeds = sort keys %$feed_list; # walk through feeds... for my $feed (@sorted_feeds) { my $base = $feed; $base =~ s/^.*\.//; # add widgets... $feeds->add($feed, -text => $base); # skip counting if group... next if !$feed_list->{$feed}; # count read and unread articles in feed... my @counts; for my $i (0, 1) { my $r = $sql->select_one_row( 'select count(1) from articles' . ' where feed=?' . ($i ? q/ and read='0'/ : q//), $feed) or warn $sql->get_error; $counts[$i] = $r->{'count(1)'}; } $feed_counts{$feed} = [ @counts ]; } # compute and fill in the blanks for groups... for my $feed (@sorted_feeds) { if (not exists $feed_counts{$feed}) { $feed_counts{$feed} = [0, 0]; next; } update_count($feed, @{$feed_counts{$feed}}); } display_counts(); # make the groups initially closed... for my $feed (@sorted_feeds) { next if $feed_list->{$feed}; $feeds->setmode($feed, 'close'); $feeds->close($feed); } } # refresh (USR1) handler sub do_refresh { # obliterate all and load feeds... load_feeds(); clear_articles(); clear_one_article(); # if current feed still exists, make it seen and selected and # load its articles... if ($feeds->info('exists', $current_feed)) { $feeds->see($current_feed); $feeds->anchorSet($current_feed); load_articles(); # dtto if current article still exists... if ($articles->info('exists', $current_article)) { $articles->see($current_article); $articles->anchorSet($current_article); load_one_article; } else { $current_article = -1; } } else { $current_feed = q//; } } # signal handlers sub sig_usr1 { $mw->afterIdle(\&do_refresh); $SIG{USR1} = \&sig_usr1; } sub sig_chld { while (waitpid(-1, WNOHANG) > 0) {}; $SIG{CHLD} = \&sig_chld; } @SIG{qw/CHLD USR1/} = (\&sig_chld, \&sig_usr1); # load and run! setup_widgets(); load_feeds(); MainLoop();