From: Amitai Schlair Date: Sun, 12 May 2013 23:16:50 +0000 (-0400) Subject: Merge branch 'master' into fancypodcast X-Git-Tag: 3.20130904~88^2~9 X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/commitdiff_plain/d33b31f70638025f19fdfa03a8c2e609faca792a?hp=69525cf0fcd02874683c9e08e5c4eae82023b681 Merge branch 'master' into fancypodcast --- diff --git a/IkiWiki/Plugin/inline.pm b/IkiWiki/Plugin/inline.pm index 8eb033951..e313eb775 100644 --- a/IkiWiki/Plugin/inline.pm +++ b/IkiWiki/Plugin/inline.pm @@ -611,6 +611,26 @@ sub absolute_urls ($$) { return $ret; } +sub genenclosure { + my $itemtemplate=shift; + my $url=shift; + my $file=shift; + + return unless $itemtemplate->query(name => "enclosure"); + + my $size=(srcfile_stat($file))[8]; + my $mime="unknown"; + eval q{use File::MimeInfo}; + if (! $@) { + $mime = mimetype($file); + } + $itemtemplate->param( + enclosure => $url, + type => $mime, + length => $size, + ); +} + sub genfeed ($$$$$@) { my $feedtype=shift; my $feedurl=shift; @@ -627,6 +647,7 @@ sub genfeed ($$$$$@) { foreach my $p (@pages) { my $u=URI->new(encode_utf8(urlto($p, "", 1))); my $pcontent = absolute_urls(get_inline_content($p, $page), $url); + my $fancy_enclosure_seen = 0; $itemtemplate->param( title => pagetitle(basename($p)), @@ -648,32 +669,27 @@ sub genfeed ($$$$$@) { $itemtemplate->param(mdate_822 => date_822($pagestate{$p}{meta}{updated})); $itemtemplate->param(mdate_3339 => date_3339($pagestate{$p}{meta}{updated})); } - } - if ($itemtemplate->query(name => "enclosure")) { - my $file=$pagesources{$p}; - my $type=pagetype($file); - if (defined $type) { - $itemtemplate->param(content => $pcontent); - } - else { - my $size=(srcfile_stat($file))[8]; - my $mime="unknown"; - eval q{use File::MimeInfo}; - if (! $@) { - $mime = mimetype($file); - } - $itemtemplate->param( - enclosure => $u, - type => $mime, - length => $size, - ); + if (exists $pagestate{$p}{meta}{enclosure}) { + my $absurl = $pagestate{$p}{meta}{enclosure}; + + # XXX better way to compute relative to srcdir? + my $file = $absurl; + $file =~ s|^$config{url}/||; + + genenclosure($itemtemplate, $absurl, $file); + $fancy_enclosure_seen = 1; } } - else { - $itemtemplate->param(content => $pcontent); + + my $file=$pagesources{$p}; + unless ($fancy_enclosure_seen || defined(pagetype($file))) { + genenclosure($itemtemplate, $u, $file); + $itemtemplate->param(simplepodcast => 1); } + $itemtemplate->param(content => $pcontent); + run_hooks(pagetemplate => sub { shift->(page => $p, destpage => $page, template => $itemtemplate); @@ -694,6 +710,7 @@ sub genfeed ($$$$$@) { feeddesc => $feeddesc, guid => $guid, feeddate => date_3339($lasttime), + feeddate_822 => date_822($lasttime), feedurl => $feedurl, ); run_hooks(pagetemplate => sub { diff --git a/IkiWiki/Plugin/meta.pm b/IkiWiki/Plugin/meta.pm index 7ea70b5d1..794f6d861 100644 --- a/IkiWiki/Plugin/meta.pm +++ b/IkiWiki/Plugin/meta.pm @@ -121,6 +121,17 @@ sub preprocess (@) { add_link($page, $value); return ""; } + elsif ($key eq 'enclosure') { + my $link=bestlink($page, $value); + if (! length $link) { + error gettext("enclosure not found") + } + add_depends($page, $link, deptype("presence")); + + $value=urlto($link, $page, 1); + $pagestate{$page}{meta}{enclosure}=$value; + # fallthrough + } elsif ($key eq 'author') { $pagestate{$page}{meta}{author}=$value; if (exists $params{sortas}) { @@ -318,6 +329,10 @@ sub pagetemplate (@) { $template->param(title_overridden => 1); } + if (exists $pagestate{$page}{meta}{enclosure}) { + $template->param(enclosure => HTML::Entities::encode_entities(IkiWiki::urlabs($pagestate{$page}{meta}{enclosure}, $config{url}))); + } + foreach my $field (qw{authorurl}) { eval q{use HTML::Entities}; $template->param($field => HTML::Entities::encode_entities($pagestate{$page}{meta}{$field})) diff --git a/doc/features.mdwn b/doc/features.mdwn index 66f7ecb73..5bbe5ef21 100644 --- a/doc/features.mdwn +++ b/doc/features.mdwn @@ -64,9 +64,11 @@ Ikiwiki can also [[plugins/aggregate]] external blogs, feeding them into the wiki. This can be used to create a Planet type site that aggregates interesting feeds. -You can also mix blogging with podcasting by dropping audio files where -they will be picked up like blog posts. This will work for any files that -you would care to syndicate. +You can also mix blogging with podcasting. Simply drop media files +where they will be picked up like blog posts. For fuller-featured +podcast feeds, enclose media files in blog posts using [[plugins/meta]]. +Either way, this will work for any files that you would care to +syndicate. ## Valid html and [[css]] diff --git a/doc/ikiwiki/directive/inline.mdwn b/doc/ikiwiki/directive/inline.mdwn index a9c241afc..c0d4e035b 100644 --- a/doc/ikiwiki/directive/inline.mdwn +++ b/doc/ikiwiki/directive/inline.mdwn @@ -11,7 +11,8 @@ Any pages that match the specified [[PageSpec]] (in the example, any [[SubPage]] of "blog") will be part of the blog, and the newest 10 of them will appear in the page. Note that if files that are not pages match the [[PageSpec]], they will be included in the feed using RSS -enclosures, which is useful for podcasting. +enclosures, which is useful for simple podcasting; for fuller-featured +podcast feeds, enclose media files in blog posts using [[meta]]. The optional `rootpage` parameter tells the wiki that new posts to this blog should default to being [[SubPages|SubPage]] of "blog", and enables a diff --git a/doc/ikiwiki/directive/meta.mdwn b/doc/ikiwiki/directive/meta.mdwn index 984f68540..fbbffa575 100644 --- a/doc/ikiwiki/directive/meta.mdwn +++ b/doc/ikiwiki/directive/meta.mdwn @@ -136,6 +136,11 @@ Supported fields: [[!iki plugins/htmlscrubber desc=htmlscrubber]] plugin is enabled, since it can be used to insert unsafe content. +* enclosure + + Specifies a link to a file to be rendered as an "enclosure" in + RSS/Atom feeds (and a plain old link in HTML). Useful for podcasting. + * redir Causes the page to redirect to another page in the wiki. diff --git a/doc/style.css b/doc/style.css index 424d43816..81b6a0a28 100644 --- a/doc/style.css +++ b/doc/style.css @@ -58,7 +58,8 @@ nav { border-bottom: 1px solid #000; } -.inlinecontent { +.inlinecontent, +.inlineenclosure { margin-top: .4em; } diff --git a/t/podcast.t b/t/podcast.t new file mode 100755 index 000000000..a00545b98 --- /dev/null +++ b/t/podcast.t @@ -0,0 +1,233 @@ +#!/usr/bin/perl +use warnings; +use strict; + +BEGIN { + eval q{use XML::Feed; use HTML::Parser; use HTML::LinkExtor}; + if ($@) { + eval q{use Test::More skip_all => + "XML::Feed and/or HTML::Parser not available"}; + } + else { + eval q{use Test::More tests => 136}; + } +} + +use Cwd; +use File::Basename; + +my $tmp = 't/tmp'; +my $statedir = 't/tinypodcast/.ikiwiki'; + +sub podcast { + my $podcast_style = shift; + + my $baseurl = 'http://example.com'; + my @command = (qw(./ikiwiki.out -plugin inline -rss -atom)); + push @command, qw(-underlaydir=underlays/basewiki); + push @command, qw(-set underlaydirbase=underlays -templatedir=templates); + push @command, "-url=$baseurl", qw(t/tinypodcast), "$tmp/out"; + + ok(! system("mkdir $tmp"), + q{setup}); + ok(! system(@command), + q{build}); + + my %media_types = ( + 'simplepost' => undef, + 'piano.mp3' => 'audio/mpeg', + 'scroll.3gp' => 'video/3gpp', + 'walter.ogg' => 'video/x-theora+ogg', + ); + + for my $format (qw(atom rss)) { + my $feed = XML::Feed->parse("$tmp/out/$podcast_style/index.$format"); + + is($feed->title, $podcast_style, + qq{$format feed title}); + is($feed->link, "$baseurl/$podcast_style/", + qq{$format feed link}); + is($feed->description, 'wiki', + qq{$format feed description}); + if ('atom' eq $format) { + is($feed->author, $feed->description, + qq{$format feed author}); + is($feed->id, $feed->link, + qq{$format feed id}); + is($feed->generator, "ikiwiki", + qq{$format feed generator}); + } + + for my $entry ($feed->entries) { + my $title = $entry->title; + my $url = $entry->id; + my $body = $entry->content->body; + my $enclosure = $entry->enclosure; + + is($entry->link, $url, qq{$format $title link}); + isnt($entry->issued, undef, + qq{$format $title issued date}); + isnt($entry->modified, undef, + qq{$format $title modified date}); + + if (defined $media_types{$title}) { + is($url, "$baseurl/$title", + qq{$format $title id}); + is($body, undef, + qq{$format $title no body text}); + is($enclosure->url, $url, + qq{$format $title enclosure url}); + is($enclosure->type, $media_types{$title}, + qq{$format $title enclosure type}); + cmp_ok($enclosure->length, '>', 0, + qq{$format $title enclosure length}); + } + else { + # XXX hack hack hack + my $expected_id = "$baseurl/$title/"; + $expected_id =~ s/\ /_/g; + + is($url, $expected_id, + qq{$format $title id}); + isnt($body, undef, + qq{$format $title body text}); + + if ('fancy' eq $podcast_style) { + isnt($enclosure, undef, + qq{$format $title enclosure}); + my $filename = basename($enclosure->url); + is($enclosure->type, $media_types{$filename}, + qq{$format $title enclosure type}); + cmp_ok($enclosure->length, '>', 0, + qq{$format $title enclosure length}); + } + else { + is($enclosure, undef, + qq{$format $title no enclosure}); + } + } + } + } + + ok(! system("rm -rf $tmp $statedir"), q{teardown}); +} + +sub single_page_html { + my @command = (qw(./ikiwiki.out)); + push @command, qw(-underlaydir=underlays/basewiki); + push @command, qw(-set underlaydirbase=underlays -templatedir=templates); + push @command, qw(t/tinypodcast), "$tmp/out"; + + ok(! system("mkdir $tmp"), + q{setup}); + ok(! system(@command), + q{build}); + + my $html = "$tmp/out/pianopost/index.html"; + like(_extract_html_content($html, 'content'), qr/has content and/m, + q{html body text}); + like(_extract_html_content($html, 'enclosure'), qr/this episode/m, + q{html enclosure}); + my ($href) = _extract_html_links($html, 'piano'); + is($href, '/piano.mp3', + q{html enclosure sans -url is site-absolute}); + + $html = "$tmp/out/attempted_multiple_enclosures/index.html"; + like(_extract_html_content($html, 'content'), qr/has content and/m, + q{html body text}); + like(_extract_html_content($html, 'enclosure'), qr/this episode/m, + q{html enclosure}); + ($href) = _extract_html_links($html, 'walter'); + is($href, '/walter.ogg', + q{html enclosure sans -url is site-absolute}); + + my $baseurl = 'http://example.com'; + ok(! system(@command, "-url=$baseurl", q{--rebuild})); + + $html = "$tmp/out/pianopost/index.html"; + ($href) = _extract_html_links($html, 'piano'); + is($href, "$baseurl/piano.mp3", + q{html enclosure with -url is fully absolute}); + + $html = "$tmp/out/attempted_multiple_enclosures/index.html"; + ($href) = _extract_html_links($html, 'walter'); + is($href, "$baseurl/walter.ogg", + q{html enclosure with -url is fully absolute}); + + ok(! system("rm -rf $tmp $statedir"), q{teardown}); +} + +sub inlined_pages_html { + my @command = (qw(./ikiwiki.out -plugin inline)); + push @command, qw(-underlaydir=underlays/basewiki); + push @command, qw(-set underlaydirbase=underlays -templatedir=templates); + push @command, qw(t/tinypodcast), "$tmp/out"; + + ok(! system("mkdir $tmp"), + q{setup}); + ok(! system(@command), + q{build}); + + my $html = "$tmp/out/fancy/index.html"; + my $contents = _extract_html_content($html, 'content'); + like($contents, qr/has content and an/m, + q{html body text from pianopost}); + like($contents, qr/has content and only one/m, + q{html body text from attempted_multiple_enclosures}); + my $enclosures = _extract_html_content($html, 'inlineenclosure'); + like($enclosures, qr/this episode/m, + q{html enclosure}); + my ($href) = _extract_html_links($html, 'piano.mp3'); + is($href, '/piano.mp3', + q{html enclosure from pianopost sans -url}); + ($href) = _extract_html_links($html, 'walter.ogg'); + is($href, '/walter.ogg', + q{html enclosure from attempted_multiple_enclosures sans -url}); + + ok(! system("rm -rf $tmp $statedir"), q{teardown}); +} + +sub _extract_html_content { + my ($file, $desired_id, $desired_tag) = @_; + $desired_tag = 'div' unless defined $desired_tag; + + my $p = HTML::Parser->new(api_version => 3); + my $content = ''; + + $p->handler(start => sub { + my ($tag, $self, $attr) = @_; + return if $tag ne $desired_tag; + return unless exists $attr->{id} && $attr->{id} eq $desired_id; + + $self->handler(text => sub { + my ($dtext) = @_; + $content .= $dtext; + }, "dtext"); + }, "tagname,self,attr"); + + $p->parse_file($file) || die $!; + + return $content; +} + +sub _extract_html_links { + my ($file, $desired_value) = @_; + + my @hrefs = (); + + my $p = HTML::LinkExtor->new(sub { + my ($tag, %attr) = @_; + return if $tag ne 'a'; + return unless $attr{href} =~ qr/$desired_value/; + push(@hrefs, values %attr); + }, getcwd() . '/' . $file); + + $p->parse_file($file); + + return @hrefs; +} + +podcast('simple'); +single_page_html(); +inlined_pages_html(); +podcast('fancy'); diff --git a/t/tinypodcast/attempted_multiple_enclosures.mdwn b/t/tinypodcast/attempted_multiple_enclosures.mdwn new file mode 100644 index 000000000..ea7bae8d0 --- /dev/null +++ b/t/tinypodcast/attempted_multiple_enclosures.mdwn @@ -0,0 +1,4 @@ +[[!meta enclosure="piano.mp3" enclosure="scroll.3gp"]] +[[!meta enclosure="walter.ogg"]] + +this article has content _and_ only one enclosure! diff --git a/t/tinypodcast/fancy.mdwn b/t/tinypodcast/fancy.mdwn new file mode 100644 index 000000000..290f4c2fd --- /dev/null +++ b/t/tinypodcast/fancy.mdwn @@ -0,0 +1 @@ +[[!inline pages="pianopost or attempted_multiple_enclosures"]] diff --git a/t/tinypodcast/piano.mp3 b/t/tinypodcast/piano.mp3 new file mode 100644 index 000000000..3d6b6625a Binary files /dev/null and b/t/tinypodcast/piano.mp3 differ diff --git a/t/tinypodcast/pianopost.mdwn b/t/tinypodcast/pianopost.mdwn new file mode 100644 index 000000000..b02f8dee0 --- /dev/null +++ b/t/tinypodcast/pianopost.mdwn @@ -0,0 +1,3 @@ +[[!meta enclosure="piano.mp3"]] + +this article has content _and_ an enclosure! diff --git a/t/tinypodcast/scroll.3gp b/t/tinypodcast/scroll.3gp new file mode 100644 index 000000000..61e69e9da Binary files /dev/null and b/t/tinypodcast/scroll.3gp differ diff --git a/t/tinypodcast/simple.mdwn b/t/tinypodcast/simple.mdwn new file mode 100644 index 000000000..052369780 --- /dev/null +++ b/t/tinypodcast/simple.mdwn @@ -0,0 +1 @@ +[[!inline pages="simplepost or *.3gp or *.mov or *.mp3 or *.ogg"]] diff --git a/t/tinypodcast/simplepost.mdwn b/t/tinypodcast/simplepost.mdwn new file mode 100644 index 000000000..d28ddb524 --- /dev/null +++ b/t/tinypodcast/simplepost.mdwn @@ -0,0 +1 @@ +this article has content but no enclosure diff --git a/t/tinypodcast/walter.ogg b/t/tinypodcast/walter.ogg new file mode 100644 index 000000000..3eee48cd6 Binary files /dev/null and b/t/tinypodcast/walter.ogg differ diff --git a/templates/atomitem.tmpl b/templates/atomitem.tmpl index 4ed17bc62..9b056e0f4 100644 --- a/templates/atomitem.tmpl +++ b/templates/atomitem.tmpl @@ -34,11 +34,12 @@ - + + - + diff --git a/templates/inlinepage.tmpl b/templates/inlinepage.tmpl index b0b53d041..cf0b6037e 100644 --- a/templates/inlinepage.tmpl +++ b/templates/inlinepage.tmpl @@ -23,6 +23,12 @@ +
+