From e8e63caba604673ca267bd492ac8162a476e710c Mon Sep 17 00:00:00 2001
From: Aaron Parecki
Date: Sat, 11 Nov 2017 07:34:24 -0800
Subject: [PATCH] implements parsing Atom and RSS feeds
---
composer.json | 3 +-
composer.lock | 100 +++++++-
lib/XRay/Formats/Format.php | 4 +
lib/XRay/Formats/Mf2.php | 12 +-
lib/XRay/Formats/XML.php | 92 ++++++++
lib/XRay/Parser.php | 4 +
tests/FeedTest.php | 125 ++++++++--
tests/data/feed.example.com/atom | 201 ++++++++++++++++
tests/data/feed.example.com/podcast-rss | 297 ++++++++++++++++++++++++
tests/data/feed.example.com/rss | 197 ++++++++++++++++
10 files changed, 1006 insertions(+), 29 deletions(-)
create mode 100644 lib/XRay/Formats/XML.php
create mode 100644 tests/data/feed.example.com/atom
create mode 100644 tests/data/feed.example.com/podcast-rss
create mode 100644 tests/data/feed.example.com/rss
diff --git a/composer.json b/composer.json
index 94d6f6a..4643f83 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
"dg/twitter-php": "3.6.*",
"p3k/timezone": "*",
"p3k/http": "0.1.*",
- "cebe/markdown": "1.1.*"
+ "cebe/markdown": "1.1.*",
+ "miniflux/picofeed": "^0.1.37"
},
"autoload": {
"psr-4": {
diff --git a/composer.lock b/composer.lock
index 79cf6c0..146a69a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "082e2019c20a5d611f58716d3f75a131",
+ "content-hash": "4cacb7d73963baec7b3fbc3248e43833",
"packages": [
{
"name": "cebe/markdown",
@@ -256,6 +256,59 @@
],
"time": "2017-05-27T15:27:47+00:00"
},
+ {
+ "name": "miniflux/picofeed",
+ "version": "v0.1.37",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/miniflux/picoFeed.git",
+ "reference": "402b7f07629577e7929625e78bc88d3d5831a22d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/miniflux/picoFeed/zipball/402b7f07629577e7929625e78bc88d3d5831a22d",
+ "reference": "402b7f07629577e7929625e78bc88d3d5831a22d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "php": ">=5.3.0",
+ "zendframework/zendxml": "^1.0"
+ },
+ "require-dev": {
+ "phpdocumentor/reflection-docblock": "2.0.4",
+ "phpunit/phpunit": "4.8.26",
+ "symfony/yaml": "2.8.7"
+ },
+ "suggest": {
+ "ext-curl": "PicoFeed will use cURL if present"
+ },
+ "bin": [
+ "picofeed"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PicoFeed": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frédéric Guillot"
+ }
+ ],
+ "description": "Modern library to handle RSS/Atom feeds",
+ "homepage": "https://github.com/miniflux/picoFeed",
+ "time": "2017-11-02T03:20:36+00:00"
+ },
{
"name": "p3k/http",
"version": "0.1.5",
@@ -328,6 +381,51 @@
"timezone"
],
"time": "2017-01-12T17:30:08+00:00"
+ },
+ {
+ "name": "zendframework/zendxml",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/ZendXml.git",
+ "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/ZendXml/zipball/7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
+ "reference": "7b64507bc35d841c9c5802d67f6f87ef8e1a58c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^3.7 || ^4.0",
+ "squizlabs/php_codesniffer": "^1.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "ZendXml\\": "library/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Utility library for XML usage, best practices, and security in PHP",
+ "homepage": "http://packages.zendframework.com/",
+ "keywords": [
+ "security",
+ "xml",
+ "zf2"
+ ],
+ "time": "2016-02-04T21:02:08+00:00"
}
],
"packages-dev": [
diff --git a/lib/XRay/Formats/Format.php b/lib/XRay/Formats/Format.php
index 0c94101..135c59d 100644
--- a/lib/XRay/Formats/Format.php
+++ b/lib/XRay/Formats/Format.php
@@ -81,5 +81,9 @@ abstract class Format implements iFormat {
return $sanitized;
}
+ protected static function stripHTML($html) {
+ return trim(strip_tags($html));
+ }
+
}
diff --git a/lib/XRay/Formats/Mf2.php b/lib/XRay/Formats/Mf2.php
index 33ee529..05e9fbb 100644
--- a/lib/XRay/Formats/Mf2.php
+++ b/lib/XRay/Formats/Mf2.php
@@ -535,18 +535,12 @@ class Mf2 extends Format {
private static function parseAsHFeed($mf2, $http) {
$data = [
'type' => 'feed',
- 'author' => [
- 'type' => 'card',
- 'name' => null,
- 'url' => null,
- 'photo' => null
- ],
- 'todo' => 'Not yet implemented. Please see https://github.com/aaronpk/XRay/issues/1'
+ 'todo' => 'Not yet implemented. Please see https://github.com/aaronpk/XRay/issues/1',
+ 'items' => [],
];
return [
- 'data' => $data,
- 'entries' => []
+ 'data' => $data
];
}
diff --git a/lib/XRay/Formats/XML.php b/lib/XRay/Formats/XML.php
new file mode 100644
index 0000000..27a57a6
--- /dev/null
+++ b/lib/XRay/Formats/XML.php
@@ -0,0 +1,92 @@
+ [
+ 'type' => 'unknown',
+ ],
+ 'url' => $url,
+ ];
+
+ try {
+ $reader = new Reader();
+ $parser = $reader->getParser($url, $xml, '');
+ $feed = $parser->execute();
+
+ $result['data']['type'] = 'feed';
+ $result['data']['items'] = [];
+
+ foreach($feed->getItems() as $item) {
+ $result['data']['items'][] = self::_hEntryFromFeedItem($item, $feed);
+ }
+
+ } catch(PicoFeedException $e) {
+
+ }
+
+ return $result;
+ }
+
+ private static function _hEntryFromFeedItem($item, $feed) {
+ $entry = [
+ 'type' => 'entry',
+ 'author' => [
+ 'name' => null,
+ 'url' => null,
+ 'photo' => null
+ ]
+ ];
+
+ if($item->getUrl())
+ $entry['url'] = $item->getUrl();
+
+ if($item->getPublishedDate())
+ $entry['published'] = $item->getPublishedDate()->format('c');
+
+ if($item->getTitle() && $item->getTitle() != $item->getUrl())
+ $entry['name'] = $item->getTitle();
+
+ if($item->getContent())
+ $entry['content'] = [
+ 'html' => self::sanitizeHTML($item->getContent()),
+ 'text' => self::stripHTML($item->getContent())
+ ];
+
+ if($item->getAuthor()) {
+ $entry['author']['name'] = $item->getAuthor();
+ }
+
+ if($feed->siteUrl) {
+ $entry['author']['url'] = $feed->siteUrl;
+ }
+
+ if($item->getEnclosureType()) {
+ $prop = false;
+ switch($item->getEnclosureType()) {
+ case 'audio/mpeg':
+ $prop = 'audio'; break;
+ case 'image/jpeg':
+ case 'image/png':
+ case 'image/gif':
+ $prop = 'photo'; break;
+ }
+ if($prop)
+ $entry[$prop] = $item->getEnclosureUrl();
+ }
+
+ return $entry;
+ }
+
+}
diff --git a/lib/XRay/Parser.php b/lib/XRay/Parser.php
index bb0101a..5ca3454 100644
--- a/lib/XRay/Parser.php
+++ b/lib/XRay/Parser.php
@@ -38,6 +38,10 @@ class Parser {
return Formats\Hackernews::parse($body, $url);
}
+ if(substr($body, 0, 5) == 'http, $body, $url, $opts);
}
diff --git a/tests/FeedTest.php b/tests/FeedTest.php
index 823ed94..edd763b 100644
--- a/tests/FeedTest.php
+++ b/tests/FeedTest.php
@@ -20,68 +20,157 @@ class FeedTest extends PHPUnit_Framework_TestCase {
public function testListOfHEntrys() {
$url = 'http://feed.example.com/list-of-hentrys';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('feed', $data->data->type);
+ $this->assertEquals('feed', $data->type);
}
public function testListOfHEntrysWithHCard() {
$url = 'http://feed.example.com/list-of-hentrys-with-h-card';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('feed', $data->data->type);
+ $this->assertEquals('feed', $data->type);
}
public function testShortListOfHEntrysWithHCard() {
$url = 'http://feed.example.com/short-list-of-hentrys-with-h-card';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('entry', $data->data->type);
+ $this->assertEquals('feed', $data->type);
}
public function testTopLevelHFeed() {
$url = 'http://feed.example.com/top-level-h-feed';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('feed', $data->data->type);
+ $this->assertEquals('feed', $data->type);
}
public function testHCardWithChildHEntrys() {
$url = 'http://feed.example.com/h-card-with-child-h-entrys';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('card', $data->data->type);
+ $this->assertEquals('feed', $data->type);
}
public function testHCardWithChildHFeed() {
$url = 'http://feed.example.com/h-card-with-child-h-feed';
- $response = $this->parse(['url' => $url]);
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
$body = $response->getContent();
$this->assertEquals(200, $response->getStatusCode());
- $data = json_decode($body);
+ $data = json_decode($body)->data;
- $this->assertEquals('card', $data->data->type);
+ $this->assertEquals('feed', $data->type);
+ }
+
+ public function testAtomFeed() {
+ $url = 'http://feed.example.com/atom';
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
+
+ $body = $response->getContent();
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($body)->data;
+
+ $this->assertEquals(8, count($data->items));
+ for($i=0; $i<8; $i++) {
+ $this->assertEquals('entry', $data->items[$i]->type);
+ $this->assertEquals('Tantek', $data->items[$i]->author->name);
+ $this->assertEquals('http://tantek.com/', $data->items[$i]->author->url);
+ $this->assertNotEmpty($data->items[$i]->url);
+ $this->assertNotEmpty($data->items[$i]->published);
+ $this->assertNotEmpty($data->items[$i]->content->html);
+ $this->assertNotEmpty($data->items[$i]->content->text);
+ }
+
+ $this->assertEquals('2017-11-08T23:53:00-08:00', $data->items[0]->published);
+ $this->assertEquals('http://tantek.com/2017/312/t3/tam-trail-run-first-trail-half', $data->items[0]->url);
+
+ $this->assertEquals('went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.', $data->items[1]->content->text);
+ $this->assertEquals('went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.', $data->items[1]->content->html);
+
+ $this->assertEquals('feed', $data->type);
+ }
+
+ public function testRSSFeed() {
+ $url = 'http://feed.example.com/rss';
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
+
+ $body = $response->getContent();
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($body)->data;
+
+ $this->assertEquals(10, count($data->items));
+ for($i=0; $i<10; $i++) {
+ $this->assertEquals('entry', $data->items[$i]->type);
+ $this->assertEquals('Ryan Barrett', $data->items[$i]->author->name);
+ $this->assertEquals('https://snarfed.org/', $data->items[$i]->author->url);
+ $this->assertNotEmpty($data->items[$i]->url);
+ $this->assertNotEmpty($data->items[$i]->published);
+ $this->assertNotEmpty($data->items[$i]->content->html);
+ if($i > 1)
+ $this->assertNotEmpty($data->items[$i]->content->text);
+ }
+
+ $this->assertEquals('2017-09-12T20:09:12+00:00', $data->items[9]->published);
+ $this->assertEquals('https://snarfed.org/2017-09-12_25492', $data->items[9]->url);
+ $this->assertEquals('new business cards
+
', $data->items[9]->content->html);
+ $this->assertEquals('new business cards', $data->items[9]->content->text);
+
+ $this->assertEquals('feed', $data->type);
+ }
+
+ public function testPodcastFeed() {
+ $url = 'http://feed.example.com/podcast-rss';
+ $response = $this->parse(['url' => $url, 'expect' => 'feed']);
+
+ $body = $response->getContent();
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($body)->data;
+
+ $this->assertEquals(12, count($data->items));
+ for($i=0; $i<12; $i++) {
+ $this->assertEquals('entry', $data->items[$i]->type);
+ $this->assertEquals('Aaron Parecki', $data->items[$i]->author->name);
+ $this->assertEquals('https://percolator.today/', $data->items[$i]->author->url);
+ $this->assertNotEmpty($data->items[$i]->url);
+ $this->assertNotEmpty($data->items[$i]->published);
+ $this->assertNotEmpty($data->items[$i]->name);
+ $this->assertNotEmpty($data->items[$i]->content->html);
+ $this->assertNotEmpty($data->items[$i]->content->text);
+ $this->assertNotEmpty($data->items[$i]->audio);
+ }
+
+ $this->assertEquals('Episode 1: Welcome', $data->items[11]->name);
+ $this->assertEquals('https://percolator.today/episode/1', $data->items[11]->url);
+ $this->assertEquals('2017-09-20T07:00:00+00:00', $data->items[11]->published);
+ $this->assertEquals('https://percolator.today/redirect.php?url=https%3A%2F%2Fpercolator.today%2Fmedia%2FPercolator_Episode_1.mp3', $data->items[11]->audio);
+ $this->assertContains('What is Percolator? Some thoughts about multi-photos in Instagram.', $data->items[11]->content->text);
+ $this->assertContains('What is Percolator? Some thoughts about multi-photos in Instagram.', $data->items[11]->content->html);
+ $this->assertContains('multi-photo vs collection ', $data->items[11]->content->html);
+
+ $this->assertEquals('feed', $data->type);
}
}
\ No newline at end of file
diff --git a/tests/data/feed.example.com/atom b/tests/data/feed.example.com/atom
new file mode 100644
index 0000000..c3e0c64
--- /dev/null
+++ b/tests/data/feed.example.com/atom
@@ -0,0 +1,201 @@
+HTTP/1.1 200 OK
+Server: Apache
+Date: Wed, 09 Dec 2015 03:29:14 GMT
+Content-Type: application/atom+xml; charset=utf-8
+Connection: keep-alive
+
+
+
+ Tantek Çelik
+
+
+
+ http://tantek.com/updates.atom
+
+ Tantek
+ http://tantek.com/
+
+ 2017-11-08T23:53:00-08:00
+
+ 2017-11-08T23:53:00-08:00
+ 2017-11-08T23:53:00-08:00
+
+ http://tantek.com/2017/312/t3/tam-trail-run-first-trail-half
+
+ going to Mt. Tam Trail Run 2017-11-11 07:00 at Stinson Beach!
+signed up for the half (my first trail half marathon!)
+
+
+ going to Mt. Tam Trail Run 2017-11-11 07:00 at Stinson Beach! signed up for the half (my first trail half marathon!)
+
+ note
+
+
+ 2017-11-08T23:29:00-08:00
+ 2017-11-08T23:29:00-08:00
+
+ http://tantek.com/2017/312/t2/more-pancakes-this-morning
+
+ went to MORE Pancakes! this morning @RunJanji pop-up on California st after #NPSF. Picked up a new running shirt.
+
+
+ went to MORE Pancakes! this morning
@RunJanji pop-up on California st after #
NPSF . Picked up a new running shirt.
+
+ note
+
+
+ 2017-11-08T13:19:00-08:00
+ 2017-11-08T13:19:00-08:00
+
+ http://tantek.com/2017/312/t1/w3ctpac-csswg-new-draft-barcamp
+
+ at #w3cTPAC all week:
+MT @CSSWG; I made a new draft
+W Intro’d & facilitated @W3C Plenary day #BarCamp
+ThF #WPWG #WICG
+
+
+ at #
w3cTPAC all week:
MT
@CSSWG ; I made a new draft
W Intro’d & facilitated
@W3C Plenary day #
BarCamp ThF #
WPWG #
WICG
+
+ note
+
+
+ 2017-10-31T20:16:00-07:00
+ 2017-10-31T20:16:00-07:00
+
+ http://tantek.com/2017/304/t1/run-bay-to-breakers-2018
+
+ going to run Bay to Breakers 2018-05-20 + Bonus 3k = 15k!
+join me in Corral A: https://baytobreakers.com/
+$40 today only!
+
+
+
+
+ note
+
+
+ 2017-10-30T21:20:00-07:00
+ 2017-10-30T21:20:00-07:00
+
+ http://tantek.com/2017/303/t3/amp-openwashing-like-schema
+
+ .@beep https://ethanmarcotte.com/wrote/seven-into-seven/
+& @adactio https://adactio.com/journal/13035
+I.e. #AMP is #openwashing not #openweb, similar to #schema before it.
+
+It has appearances or aspects of being open (e.g. with defensive statements like "non-google committers"), but in practice is proprietarily controlled/governed (free community labor for corporate leaders), and designed (perhaps unintentionally) to bias an otherwise seemingly open ecosystem towards a single company or small anti-competitive oligopoly in practice.
+
+Nevermind the #doublespeak of caching vs hosting, or placement within "search results" implying part of a page vs the more practically meaningful "SERP" (search engine results *page*) placement, regardless of styling as a horizontal carousel or vertical list.
+
+I still think an actually open alternative could be beneficial.
+
+Previously, previously, previously:
+* tantek.com/2017/261/t2/open-alternatives-googleamp
+ * tantek.com/2017/262/t1/amp-yourself-worse-than-html
+ * tantek.com/2017/262/t2/amp-worse-than-html-subset
+ * tantek.com/2017/263/t1/open-html-subset-interacts-quickly
+* tantek.com/2015/280/t1/js-dr-amp-not-include-javascript
+ * tantek.com/2015/280/t2/make-amp-smaller-ban-jsonld-schema-org
+
+More: https://indieweb.org/AMP
+
+
+
+
+ note
+
+
+ 2017-10-30T16:57:00-07:00
+ 2017-10-30T16:57:00-07:00
+
+ http://tantek.com/2017/303/t2/frontend-web-dev-designer-fullstack-follow-aeasf
+
+ If you’re a frontend web dev designer or fullstack-*, you must follow #AEASF for pure awesome: https://twitter.com/hashtag/aeasf?f=tweets&vertical=default
+
+
+
+
+ note
+
+
+ 2017-10-30T08:30:00-07:00
+ 2017-10-30T08:30:00-07:00
+
+ http://tantek.com/2017/303/t1/four-years-nov-project-sf-npversary
+
+ Four years ago: my first @Nov_Project_SF. #NPversary
+This past year: ran my first marathon & led #NP_Yoga
+
+#NovemberProject is an amazing community.
+
+Grateful to be a part of it, across so many cities, and especially grateful for all the support which helped me breakthrough limitations I had previously accepted, and achieve goals I had thought were impossible or had never even considered.
+
+Let’s see what I can do in year 5.
+
+Previously:
+* tantek.com/2014/308/b2/my-first-year-november-project
+* tantek.com/2014/303/t1/one-year-ago-justshowup-npsf-npversary
+* tantek.com/2013/303/t3/novemberproject-different-level-fitness
+
+
+
+
+ note
+
+
+ 2017-10-29T16:26:00-07:00
+ 2017-10-29T16:26:00-07:00
+
+ http://tantek.com/2017/305/e1/homebrew-website-club
+
+ Event
+
+
+
+
+
+When: 2017-11-01 17:30 …19:30
+
+Where: Mozilla San Francisco
+
+Host: Tantek Çelik
+
+
+
+17:30: Optional writing hour and quiet socializing
+18:30: IndieWeb demos and hack night
+
+
+
+Demos of personal website breakthroughs. Create or update your personal web site!
+
+
+Join a community with like-minded interests. Bring friends that want a personal site, or are interested in a healthy, independent web!
+
+
Topics for this week:
+
+
+Any questions? Ask in
+#indieweb Slack or IRC
+
+
+More information:
+IndieWeb Wiki Event Page
+
+
+RSVP: on the Facebook event or post an indie RSVP on your own site!
+
+
+
+
+
+ event
+
+
diff --git a/tests/data/feed.example.com/podcast-rss b/tests/data/feed.example.com/podcast-rss
new file mode 100644
index 0000000..11d8e47
--- /dev/null
+++ b/tests/data/feed.example.com/podcast-rss
@@ -0,0 +1,297 @@
+HTTP/1.1 200 OK
+Server: Apache
+Date: Wed, 09 Dec 2015 03:29:14 GMT
+Content-Type: application/rss+xml; charset=utf-8
+Connection: keep-alive
+
+
+
+
+
+ Percolator
+
+
+ https://percolator.today/
+ A microcast with Aaron Parecki
+ Wed, 20 Sep 2017 07:00:00 +0000
+ en-US
+ 2017 Aaron Parecki
+
+ Aaron Parecki
+ A microcast with Aaron Parecki
+
+ Aaron Parecki
+ aaron@parecki.com
+
+ No
+
+
+
+
+ -
+
Episode 12: To Poll or Not to Poll
+ https://percolator.today/episode/12
+ Fri, 10 Nov 2017 00:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/12
+ Building out a feed polling infrastructure is daunting. Thankfully Superfeedr has done it for me.
+
+
+]]>
+
+
+ No
+ No
+ 03:07
+ Aaron Parecki
+
+ -
+
Episode 11: What are Instagram Stories?
+ https://percolator.today/episode/11
+ Sat, 04 Nov 2017 00:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/11
+ A bit of a diversion from my normal routine where I implemented Instagram Stories on my own website.
+
+
+]]>
+
+
+ No
+ No
+ 03:01
+ Aaron Parecki
+
+ -
+
Episode 10: Microsub for Readers
+ https://percolator.today/episode/10
+ Fri, 03 Nov 2017 00:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/10
+ Itching to get started rebuilding Monocle.
+
+
+]]>
+
+
+ No
+ No
+ 01:44
+ Aaron Parecki
+
+ -
+
Episode 9: Streamlining my Microcast Workflow
+ https://percolator.today/episode/9
+ Mon, 30 Oct 2017 00:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/9
+ I wrote a new Micropub app to help me publish Microcast episodes quicker. You can use it too! My podcast website also receives webmentions now!
+
+
+]]>
+
+
+ No
+ No
+ 02:43
+ Aaron Parecki
+
+ -
+
Episode 8: Back from the Woods
+ https://percolator.today/episode/8
+ Sat, 28 Oct 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/8
+ Reflections on how I control what I consume online.
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 03:25
+ Aaron Parecki
+
+ -
+
Episode 7: Making Micropub More Useful
+ https://percolator.today/episode/7
+ Thu, 12 Oct 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/7
+ Thinking about what is needed for the Micropub spec for it to be even more useful.
+
+Links
+
+
+]]>
+
+
+ No
+ No
+ 05:31
+ Aaron Parecki
+
+ -
+
Episode 6: My New Superpower
+ https://percolator.today/episode/6
+ Mon, 09 Oct 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/6
+ My Overland GPS tracker app is live in the app store!
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 03:26
+ Aaron Parecki
+
+ -
+
Episode 5: Homebrew Website Club
+ https://percolator.today/episode/5
+ Fri, 06 Oct 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/5
+ We had a great Portland Homebrew Website Club!
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 02:10
+ Aaron Parecki
+
+ -
+
Episode 4: Pretending to be an iOS Developer
+ https://percolator.today/episode/4
+ Wed, 04 Oct 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/4
+ Talking about what it takes to publish an app on the iOS app store.
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 05:24
+ Aaron Parecki
+
+ -
+
Episode 3: Following
+ https://percolator.today/episode/3
+ Fri, 29 Sep 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/3
+ Thinking about how I want to follow people on the IndieWeb.
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 05:00
+ Aaron Parecki
+
+ -
+
Episode 2: Publishing a Podcast
+ https://percolator.today/episode/2
+ Fri, 22 Sep 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/2
+ Building a website for this podcast, enjoying a new coffee shop.
+
+Foxy coffee
+]]>
+
+
+ No
+ No
+ 04:02
+ Aaron Parecki
+
+ -
+
Episode 1: Welcome
+ https://percolator.today/episode/1
+ Wed, 20 Sep 2017 07:00:00 +0000
+ Aaron Parecki
+ https://percolator.today/episode/1
+ What is Percolator? Some thoughts about multi-photos in Instagram.
+
+Links:
+
+
+]]>
+
+
+ No
+ No
+ 03:15
+ Aaron Parecki
+
+
+
diff --git a/tests/data/feed.example.com/rss b/tests/data/feed.example.com/rss
new file mode 100644
index 0000000..5c687a5
--- /dev/null
+++ b/tests/data/feed.example.com/rss
@@ -0,0 +1,197 @@
+HTTP/1.1 200 OK
+Server: Apache
+Date: Wed, 09 Dec 2015 03:29:14 GMT
+Content-Type: application/rss+xml; charset=utf-8
+Connection: keep-alive
+
+
+
+
+ snarfed.org
+
+ https://snarfed.org
+ Ryan Barrett's blog
+ Sat, 11 Nov 2017 00:30:06 +0000
+ en-US
+ hourly
+ 1
+ https://wordpress.org/?v=4.8.3
+ -
+
+ https://snarfed.org/2017-11-05_25889
+ https://snarfed.org/2017-11-05_25889#comments
+ Mon, 06 Nov 2017 02:15:02 +0000
+
+
+
+ https://snarfed.org/?p=25889
+
+
+]]>
+ https://snarfed.org/2017-11-05_25889/feed
+ 12
+
+ -
+
+ https://snarfed.org/2017-10-31_25851
+ https://snarfed.org/2017-10-31_25851#comments
+ Tue, 31 Oct 2017 15:28:04 +0000
+
+
+
+ https://snarfed.org/2017-10-31_25851
+
+
+]]>
+ https://snarfed.org/2017-10-31_25851/feed
+ 38
+
+ -
+
+ https://snarfed.org/2017-10-25_25812
+ https://snarfed.org/2017-10-25_25812#respond
+ Wed, 25 Oct 2017 17:01:49 +0000
+
+
+
+ https://snarfed.org/?p=25812
+
+ Whoa. Microsoft is killing the Kinect. It’s been a while coming, I guess, but still. End of an era!
+]]>
+ https://snarfed.org/2017-10-25_25812/feed
+ 0
+
+ -
+
+ https://snarfed.org/2017-10-23_25792
+ https://snarfed.org/2017-10-23_25792#respond
+ Mon, 23 Oct 2017 20:06:25 +0000
+
+
+
+ https://snarfed.org/?p=25792
+
+
+ Nothing as good will ever be built again, so let us make proper use of what we’ve got.
+
+
+ – Clerk of Works, Chatsworth Estate , ~1960
+ via Deborah Cavendish, Duchess of Devonshire , The Estate
+ via Stewart Brand, How Buildings Learn
+]]>
+ https://snarfed.org/2017-10-23_25792/feed
+ 0
+
+ -
+
Bridgy Fed
+ https://snarfed.org/2017-10-22_bridgy-fed
+ https://snarfed.org/2017-10-22_bridgy-fed#respond
+ Mon, 23 Oct 2017 04:01:46 +0000
+
+
+
+ https://snarfed.org/?p=25785
+
+
+
+
+
+I’m launching a new side project! Bridgy Fed connects your IndieWeb site with federated social networks like Mastodon and Hubzilla . If your site can send and receive webmentions , Bridgy Fed can federate it with the fediverse !
+
+I originally discovered the IndieWeb after I started connecting this web site with social networks like Facebook and Twitter. That led to Bridgy , which has grown bigger than I care to admit. I still have that desire to bridge different communities and networks, and Bridgy Fed is just the latest incarnation. Give it a whirl, kick the tires, and let me know what you think !
+]]>
+ https://snarfed.org/2017-10-22_bridgy-fed/feed
+ 0
+
+ -
+
+ https://snarfed.org/2017-10-15_25692
+ https://snarfed.org/2017-10-15_25692#comments
+ Sun, 15 Oct 2017 13:43:58 +0000
+
+
+
+ https://snarfed.org/?p=25692
+
+ That feeling when you Google something obscure and only find your own old stuff.
+]]>
+ https://snarfed.org/2017-10-15_25692/feed
+ 21
+
+ -
+
+ https://snarfed.org/2017-10-10_25648
+ https://snarfed.org/2017-10-10_25648#comments
+ Tue, 10 Oct 2017 18:11:51 +0000
+
+
+
+ https://snarfed.org/2017-10-10_25648
+
+ Found this front page of the Feb. 18, 1976 New Nigerian blowing down our street in San Francisco.
+
+
+]]>
+ https://snarfed.org/2017-10-10_25648/feed
+ 11
+
+ -
+
+ https://snarfed.org/2017-10-08_25636
+ https://snarfed.org/2017-10-08_25636#comments
+ Sun, 08 Oct 2017 17:07:08 +0000
+
+
+
+ https://snarfed.org/2017-10-08_25636
+
+ Seeing more and more cards with red edges these days. Different networks, issuing banks, etc. What’s the secret?
+
+
+]]>
+ https://snarfed.org/2017-10-08_25636/feed
+ 6
+
+ -
+
+ https://snarfed.org/2017-09-28_25563
+ https://snarfed.org/2017-09-28_25563#comments
+ Thu, 28 Sep 2017 13:48:30 +0000
+
+
+
+ https://snarfed.org/2017-09-28_25563
+
+ Morning, @karlthefog .
+
+
+]]>
+ https://snarfed.org/2017-09-28_25563/feed
+ 14
+
+ -
+
+ https://snarfed.org/2017-09-12_25492
+ https://snarfed.org/2017-09-12_25492#comments
+ Tue, 12 Sep 2017 20:09:12 +0000
+
+
+
+ https://snarfed.org/2017-09-12_25492
+
+ new business cards
+
+
+]]>
+ https://snarfed.org/2017-09-12_25492/feed
+ 84
+
+
+
\ No newline at end of file