root/trunk/plagger/lib/Plagger/Plugin/Publish/Feed.pm

Revision 1959 (checked in by otsune, 10 months ago)

- Add copyright.
- fix add_enclosure in RSS (see CAVEATS at perldoc XML::RSS::LibXML).

Line 
1 package Plagger::Plugin::Publish::Feed;
2
3 use strict;
4 use base qw( Plagger::Plugin );
5
6 use XML::Feed;
7 use XML::Feed::Entry;
8 use XML::Feed::RSS; # load explicitly to force LibXML
9 use XML::RSS::LibXML;
10 use File::Spec;
11
12 $XML::Feed::RSS::PREFERRED_PARSER = "XML::RSS::LibXML";
13
14 sub register {
15     my($self, $context) = @_;
16     $context->autoload_plugin({ module => 'Filter::FloatingDateTime' });
17     $context->register_hook(
18         $self,
19         'publish.feed' => \&publish_feed,
20         'plugin.init'  => \&plugin_init,
21     );
22 }
23
24 sub plugin_init {
25     my($self, $context, $args) = @_;
26
27     # check dir
28     my $dir = $self->conf->{dir};
29     unless (-e $dir && -d _) {
30         mkdir $dir, 0755 or $context->error("mkdir $dir: $!");
31     }
32
33     unless (exists $self->conf->{full_content}) {
34         $self->conf->{full_content} = 1;
35     }
36 }
37
38 sub publish_feed {
39     my($self, $context, $args) = @_;
40
41     my $conf = $self->conf;
42     my $f = $args->{feed};
43     my $feed_format = $conf->{format} || 'Atom';
44
45     # generate feed
46     my $feed = XML::Feed->new($feed_format);
47     $feed->title($f->title);
48     $feed->link($f->link);
49     $feed->modified(Plagger::Date->now);
50     $feed->generator("Plagger/$Plagger::VERSION");
51     $feed->description($f->description);
52     $feed->copyright($f->meta->{copyright}) if $f->meta->{copyright};
53     $feed->author( $self->make_author($f->author, $feed_format) )
54         if $f->primary_author;
55
56     my $taguri_base = $self->conf->{taguri_base} || do {
57         require Sys::Hostname;
58         Sys::Hostname::hostname();
59     };
60
61     if ($feed_format eq 'Atom') {
62         $feed->{atom}->id("tag:$taguri_base,2006:" . $f->id); # XXX what if id is empty?
63     }
64
65     # add entry
66     for my $e ($f->entries) {
67         my $entry = XML::Feed::Entry->new($feed_format);
68         $entry->title($e->title);
69         $entry->link($e->permalink);
70         $entry->summary($e->body_text) if defined $e->body;
71
72         # hack to bypass XML::Feed Atom 0.3 crufts (type="text/html")
73         if ($self->conf->{full_content} && defined $e->body) {
74             if ($feed_format eq 'RSS') {
75                 $entry->content($e->body);
76             } else {
77                 $entry->{entry}->content($e->body->utf8);
78             }
79         }
80
81         $entry->category(join(' ', @{$e->tags})) if @{$e->tags};
82         $entry->issued($e->date)   if $e->date;
83         $entry->modified($e->date) if $e->date;
84
85         if ($feed_format eq 'RSS') {
86             my $author = 'nobody@example.com';
87             $author .= ' (' . $e->author . ')' if $e->author;
88             $entry->author($author);
89         } else {
90             unless ($feed->author) {
91                 $entry->author($e->author || 'nobody');
92             }
93         }
94
95         $entry->id("tag:$taguri_base,2006:" . $e->id);
96
97         if ($e->has_enclosure) {
98             for my $enclosure (grep { defined $_->url && !$_->is_inline } $e->enclosures) {
99                 $entry->add_enclosure({
100                     url    => $enclosure->url,
101                     length => $enclosure->length,
102                     type   => $enclosure->type,
103                 });
104
105                 # RSS 2.0 by spec doesn't allow multiple enclosures
106                 last if $feed_format eq 'RSS';
107             }
108         }
109
110         $feed->add_entry($entry);
111     }
112
113     # generate file path
114     my $tmpl = '%i.' . ($feed_format eq 'RSS' ? 'rss' : 'atom');
115     my $file = Plagger::Util::filename_for($f, $self->conf->{filename} || $tmpl);
116     my $filepath = File::Spec->catfile($self->conf->{dir}, $file);
117
118     $context->log(info => "save feed for " . $f->link . " to $filepath");
119
120     my $xml = $feed->as_xml;
121     utf8::decode($xml) unless utf8::is_utf8($xml);
122     open my $output, ">:utf8", $filepath or $context->error("$filepath: $!");
123     print $output $xml;
124     close $output;
125 }
126
127 sub make_author {
128     my($self, $author, $feed_format) = @_;
129
130     if ($feed_format eq 'RSS') {
131         my $rfc822 = 'nobody@example.com';
132         $rfc822 .= ' (' . $author . ')' if $author;
133         return $rfc822;
134     } else {
135         return defined $author ? $author : 'nobody';
136     }
137 }
138
139 # XXX okay, this is a hack until XML::Feed is updated
140 *XML::Feed::Entry::Atom::add_enclosure = sub {
141     my($entry, $enclosure) = @_;
142     my $link = XML::Atom::Link->new;
143     $link->rel('enclosure');
144     $link->type($enclosure->{type});
145     $link->href($enclosure->{url});
146     $link->length($enclosure->{length});
147     $entry->{entry}->add_link($link);
148 };
149
150 *XML::Feed::Entry::RSS::add_enclosure = sub {
151     my($entry, $enclosure) = @_;
152     $entry->{entry}->{enclosure} = XML::RSS::LibXML::MagicElement->new(
153         attributes => {
154             url    => $enclosure->{url},
155             type   => $enclosure->{type},
156             length => $enclosure->{length},
157         }
158     );
159 };
160
161 1;
162
163 __END__
164
165 =head1
166
167 Plagger::Plugin::Publish::Feed - republish RSS/Atom feeds
168
169 =head1 SYNOPSIS
170
171   - module: Publish::Feed
172     config:
173       format: RSS
174       dir: /home/yoshiki/plagger/feed
175       filename: my_%t.rss
176
177 =head1 CONFIG
178
179 =over 4
180
181 =item format
182
183 Specify the format of feed. C<Plagger::Plugin::Publish::Feed> supports
184 the following syndication feed formats:
185
186 =over 8
187
188 =item Atom (default)
189
190 =item RSS
191
192 =back
193
194 =item dir
195
196 Directory to save feed files in.
197
198 =item filename
199
200 Filename to be used to create feed files. It defaults to C<%i.rss> for
201 RSS and C<%i.atom> for Atom feed. It supports the following format
202 like printf():
203
204 =over 8
205
206 =item %u url
207
208 =item %l link
209
210 =item %t title
211
212 =item %i id
213
214 =back
215
216 =item full_content
217
218 Whether to publish full content feed. Defaults to 1.
219
220 =item taguri_base
221
222 Domain name to use with Tag URI base for Atom feed IDs. If it's not
223 set, the domain is grabbed using Sys::Hostname module Optional.
224
225 =back
226
227 =head1 AUTHOR
228
229 Tatsuhiko Miyagawa
230
231 =head1 CONTRIBUTORS
232
233 Yoshiki Kurihara
234
235 Gosuke Miyashita
236
237 =head1 SEE ALSO
238
239 L<Plagger>, L<XML::Feed>
240
241 =cut
Note: See TracBrowser for help on using the browser.