Parse enclosures in RSS.

This commit is contained in:
Brent Simmons 2017-12-18 15:35:50 -08:00
parent 7d68e3322c
commit fb9a1d610c
10 changed files with 251 additions and 3 deletions

View File

@ -19,6 +19,10 @@ public struct ParsedAttachment: Hashable {
init?(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) { init?(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) {
if url.isEmpty {
return nil
}
self.url = url self.url = url
self.mimeType = mimeType self.mimeType = mimeType
self.title = title self.title = title

View File

@ -8,6 +8,7 @@
@import Foundation; @import Foundation;
@class RSParsedEnclosure;
@interface RSParsedArticle : NSObject @interface RSParsedArticle : NSObject
@ -22,6 +23,7 @@
@property (nonatomic, nullable) NSString *link; @property (nonatomic, nullable) NSString *link;
@property (nonatomic, nullable) NSString *permalink; @property (nonatomic, nullable) NSString *permalink;
@property (nonatomic, nullable) NSString *author; @property (nonatomic, nullable) NSString *author;
@property (nonatomic, nullable) NSSet<RSParsedEnclosure *> *enclosures;
@property (nonatomic, nullable) NSDate *datePublished; @property (nonatomic, nullable) NSDate *datePublished;
@property (nonatomic, nullable) NSDate *dateModified; @property (nonatomic, nullable) NSDate *dateModified;
@property (nonatomic, nonnull) NSDate *dateParsed; @property (nonatomic, nonnull) NSDate *dateParsed;

View File

@ -0,0 +1,21 @@
//
// RSParsedEnclosure.h
// RSParser
//
// Created by Brent Simmons on 12/18/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
@interface RSParsedEnclosure : NSObject
@property (nonatomic) NSString *url;
@property (nonatomic) NSInteger length;
@property (nonatomic, nullable) NSString *mimeType;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,13 @@
//
// RSParsedEnclosure.m
// RSParser
//
// Created by Brent Simmons on 12/18/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
#import "RSParsedEnclosure.h"
@implementation RSParsedEnclosure
@end

View File

@ -41,8 +41,9 @@ private extension RSParsedFeedTransformer {
let datePublished = parsedArticle.datePublished let datePublished = parsedArticle.datePublished
let dateModified = parsedArticle.dateModified let dateModified = parsedArticle.dateModified
let authors = parsedAuthors(parsedArticle.author) let authors = parsedAuthors(parsedArticle.author)
let attachments = parsedAttachments(parsedArticle.enclosures)
return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: parsedArticle.feedURL, url: url, externalURL: externalURL, title: title, contentHTML: contentHTML, contentText: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: nil, attachments: nil) return ParsedItem(syncServiceID: nil, uniqueID: uniqueID, feedURL: parsedArticle.feedURL, url: url, externalURL: externalURL, title: title, contentHTML: contentHTML, contentText: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: nil, attachments: attachments)
} }
static func parsedAuthors(_ authorEmailAddress: String?) -> Set<ParsedAuthor>? { static func parsedAuthors(_ authorEmailAddress: String?) -> Set<ParsedAuthor>? {
@ -53,4 +54,19 @@ private extension RSParsedFeedTransformer {
let author = ParsedAuthor(name: nil, url: nil, avatarURL: nil, emailAddress: authorEmailAddress) let author = ParsedAuthor(name: nil, url: nil, avatarURL: nil, emailAddress: authorEmailAddress)
return Set([author]) return Set([author])
} }
static func parsedAttachments(_ enclosures: Set<RSParsedEnclosure>?) -> Set<ParsedAttachment>? {
guard let enclosures = enclosures, !enclosures.isEmpty else {
return nil
}
let attachments = enclosures.flatMap { (enclosure) -> ParsedAttachment? in
let sizeInBytes = enclosure.length > 0 ? enclosure.length : nil
return ParsedAttachment(url: enclosure.url, mimeType: enclosure.mimeType, title: nil, sizeInBytes: sizeInBytes, durationInSeconds: nil)
}
return attachments.isEmpty ? nil : Set(attachments)
}
} }

View File

@ -15,7 +15,7 @@
#import <RSParser/NSString+RSParser.h> #import <RSParser/NSString+RSParser.h>
#import <RSParser/RSDateParser.h> #import <RSParser/RSDateParser.h>
#import <RSParser/ParserData.h> #import <RSParser/ParserData.h>
#import <RSParser/RSParsedEnclosure.h>
@interface RSRSSParser () <RSSAXParserDelegate> @interface RSRSSParser () <RSSAXParserDelegate>
@ -159,6 +159,8 @@ static const NSInteger kTrueLength = 5;
static const char *kUppercaseRDF = "RDF"; static const char *kUppercaseRDF = "RDF";
static const NSInteger kUppercaseRDFLength = 4; static const NSInteger kUppercaseRDFLength = 4;
static const char *kEnclosure = "enclosure";
static const NSInteger kEnclosureLength = 10;
#pragma mark - Parsing #pragma mark - Parsing
@ -232,6 +234,29 @@ static const NSInteger kUppercaseRDFLength = 4;
} }
} }
- (void)addEnclosure {
NSDictionary *attributes = self.currentAttributes;
NSString *url = attributes[kURLKey];
if (!url || url.length < 1) {
return;
}
RSParsedEnclosure *enclosure = [[RSParsedEnclosure alloc] init];
enclosure.url = url;
NSString *lengthString = attributes[kLengthKey];
if (lengthString) {
enclosure.length = lengthString.integerValue;
}
enclosure.mimeType = attributes[kTypeKey];
// The RSS spec specifies zero or one enclosures.
// However, the Media RSS namespace allows for more than one.
// We could add support for multiple enclosures at some time in the future.
self.currentArticle.enclosures = [NSSet setWithObject:enclosure];
}
- (NSString *)urlString:(NSString *)s { - (NSString *)urlString:(NSString *)s {
@ -304,6 +329,9 @@ static const NSInteger kUppercaseRDFLength = 4;
else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) { else if (RSSAXEqualTags(localName, kTitle, kTitleLength)) {
self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded]; self.currentArticle.title = [self currentStringWithHTMLEntitiesDecoded];
} }
else if (RSSAXEqualTags(localName, kEnclosure, kEnclosureLength)) {
[self addEnclosure];
}
} }
@ -327,7 +355,7 @@ static const NSInteger kUppercaseRDFLength = 4;
} }
NSDictionary *xmlAttributes = nil; NSDictionary *xmlAttributes = nil;
if ((self.isRDF && RSSAXEqualTags(localName, kItem, kItemLength)) || RSSAXEqualTags(localName, kGuid, kGuidLength)) { if ((self.isRDF && RSSAXEqualTags(localName, kItem, kItemLength)) || RSSAXEqualTags(localName, kGuid, kGuidLength) || RSSAXEqualTags(localName, kEnclosure, kEnclosureLength)) {
xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes]; xmlAttributes = [self.parser attributesDictionary:attributes numberOfAttributes:numberOfAttributes];
} }
if (self.currentAttributes != xmlAttributes) { if (self.currentAttributes != xmlAttributes) {

View File

@ -37,6 +37,7 @@
#import <RSParser/RSAtomParser.h> #import <RSParser/RSAtomParser.h>
#import <RSParser/RSParsedFeed.h> #import <RSParser/RSParsedFeed.h>
#import <RSParser/RSParsedArticle.h> #import <RSParser/RSParsedArticle.h>
#import <RSParser/RSParsedEnclosure.h>
// HTML // HTML

View File

@ -7,6 +7,9 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
8401FF811FE862E70080F13F /* RSParsedEnclosure.h in Headers */ = {isa = PBXBuildFile; fileRef = 8401FF7F1FE862E70080F13F /* RSParsedEnclosure.h */; settings = {ATTRIBUTES = (Public, ); }; };
8401FF821FE862E70080F13F /* RSParsedEnclosure.m in Sources */ = {isa = PBXBuildFile; fileRef = 8401FF801FE862E70080F13F /* RSParsedEnclosure.m */; };
8401FF841FE87C2F0080F13F /* theomnishow.rss in Resources */ = {isa = PBXBuildFile; fileRef = 8401FF831FE87C2E0080F13F /* theomnishow.rss */; };
840FDCB21F01FED00041F61B /* RSSParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB11F01FED00041F61B /* RSSParserTests.swift */; }; 840FDCB21F01FED00041F61B /* RSSParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB11F01FED00041F61B /* RSSParserTests.swift */; };
840FDCB41F0217410041F61B /* JSONFeedParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB31F0217410041F61B /* JSONFeedParserTests.swift */; }; 840FDCB41F0217410041F61B /* JSONFeedParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB31F0217410041F61B /* JSONFeedParserTests.swift */; };
840FDCB61F0217D20041F61B /* AtomParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB51F0217D20041F61B /* AtomParserTests.swift */; }; 840FDCB61F0217D20041F61B /* AtomParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840FDCB51F0217D20041F61B /* AtomParserTests.swift */; };
@ -111,6 +114,9 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
8401FF7F1FE862E70080F13F /* RSParsedEnclosure.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RSParsedEnclosure.h; sourceTree = "<group>"; };
8401FF801FE862E70080F13F /* RSParsedEnclosure.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RSParsedEnclosure.m; sourceTree = "<group>"; };
8401FF831FE87C2E0080F13F /* theomnishow.rss */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = theomnishow.rss; sourceTree = "<group>"; };
840FDCB11F01FED00041F61B /* RSSParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RSSParserTests.swift; sourceTree = "<group>"; }; 840FDCB11F01FED00041F61B /* RSSParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RSSParserTests.swift; sourceTree = "<group>"; };
840FDCB31F0217410041F61B /* JSONFeedParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedParserTests.swift; sourceTree = "<group>"; }; 840FDCB31F0217410041F61B /* JSONFeedParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedParserTests.swift; sourceTree = "<group>"; };
840FDCB51F0217D20041F61B /* AtomParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomParserTests.swift; sourceTree = "<group>"; }; 840FDCB51F0217D20041F61B /* AtomParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomParserTests.swift; sourceTree = "<group>"; };
@ -343,6 +349,8 @@
84469D221EFA3134004A6B28 /* RSParsedArticle.m */, 84469D221EFA3134004A6B28 /* RSParsedArticle.m */,
84469D231EFA3134004A6B28 /* RSParsedFeed.h */, 84469D231EFA3134004A6B28 /* RSParsedFeed.h */,
84469D241EFA3134004A6B28 /* RSParsedFeed.m */, 84469D241EFA3134004A6B28 /* RSParsedFeed.m */,
8401FF7F1FE862E70080F13F /* RSParsedEnclosure.h */,
8401FF801FE862E70080F13F /* RSParsedEnclosure.m */,
); );
name = XML; name = XML;
path = Feeds/XML; path = Feeds/XML;
@ -381,6 +389,7 @@
84628AAC1FCA10AE00566A9B /* allthis.atom */, 84628AAC1FCA10AE00566A9B /* allthis.atom */,
84566D931FD0ABFB00103322 /* allthis.json */, 84566D931FD0ABFB00103322 /* allthis.json */,
84566D951FD1FC1800103322 /* allthis-partial.json */, 84566D951FD1FC1800103322 /* allthis-partial.json */,
8401FF831FE87C2E0080F13F /* theomnishow.rss */,
849A03CF1F0081EA00122600 /* Subs.opml */, 849A03CF1F0081EA00122600 /* Subs.opml */,
); );
path = Resources; path = Resources;
@ -454,6 +463,7 @@
84469CFC1EFA3069004A6B28 /* RSSAXParser.h in Headers */, 84469CFC1EFA3069004A6B28 /* RSSAXParser.h in Headers */,
845213281FCB4042003B6E93 /* RSHTMLTag.h in Headers */, 845213281FCB4042003B6E93 /* RSHTMLTag.h in Headers */,
84E7E69F1F85780D0046719D /* ParserData.h in Headers */, 84E7E69F1F85780D0046719D /* ParserData.h in Headers */,
8401FF811FE862E70080F13F /* RSParsedEnclosure.h in Headers */,
84469D071EFA307E004A6B28 /* RSHTMLLinkParser.h in Headers */, 84469D071EFA307E004A6B28 /* RSHTMLLinkParser.h in Headers */,
84469D0D1EFA307E004A6B28 /* RSSAXHTMLParser.h in Headers */, 84469D0D1EFA307E004A6B28 /* RSSAXHTMLParser.h in Headers */,
84469D2B1EFA3134004A6B28 /* RSParsedArticle.h in Headers */, 84469D2B1EFA3134004A6B28 /* RSParsedArticle.h in Headers */,
@ -561,6 +571,7 @@
840FDCBA1F02186D0041F61B /* DaringFireball.json in Resources */, 840FDCBA1F02186D0041F61B /* DaringFireball.json in Resources */,
849A03D21F0081EA00122600 /* EMarley.rss in Resources */, 849A03D21F0081EA00122600 /* EMarley.rss in Resources */,
849A03DA1F0081EA00122600 /* Subs.opml in Resources */, 849A03DA1F0081EA00122600 /* Subs.opml in Resources */,
8401FF841FE87C2F0080F13F /* theomnishow.rss in Resources */,
849A03D61F0081EA00122600 /* manton.rss in Resources */, 849A03D61F0081EA00122600 /* manton.rss in Resources */,
849A03D11F0081EA00122600 /* DaringFireball.rss in Resources */, 849A03D11F0081EA00122600 /* DaringFireball.rss in Resources */,
84B19A771FDA438300458981 /* natasha.xml in Resources */, 84B19A771FDA438300458981 /* natasha.xml in Resources */,
@ -613,6 +624,7 @@
84285AAA1F006456002E8708 /* RSParsedFeedTransformer.swift in Sources */, 84285AAA1F006456002E8708 /* RSParsedFeedTransformer.swift in Sources */,
84469D2E1EFA3134004A6B28 /* RSParsedFeed.m in Sources */, 84469D2E1EFA3134004A6B28 /* RSParsedFeed.m in Sources */,
84469CF81EFA3000004A6B28 /* RSOPMLParser.m in Sources */, 84469CF81EFA3000004A6B28 /* RSOPMLParser.m in Sources */,
8401FF821FE862E70080F13F /* RSParsedEnclosure.m in Sources */,
84469D401EFF29A9004A6B28 /* FeedParserError.swift in Sources */, 84469D401EFF29A9004A6B28 /* FeedParserError.swift in Sources */,
84469D321EFA31CF004A6B28 /* FeedParser.swift in Sources */, 84469D321EFA31CF004A6B28 /* FeedParser.swift in Sources */,
84469D281EFA3134004A6B28 /* RSAtomParser.m in Sources */, 84469D281EFA3134004A6B28 /* RSAtomParser.m in Sources */,

View File

@ -54,4 +54,20 @@ class RSSParserTests: XCTestCase {
XCTAssertEqual(parsedFeed.items.count, 10) XCTAssertEqual(parsedFeed.items.count, 10)
} }
func testTheOmniShowAttachments() {
let d = parserData("theomnishow", "rss", "https://theomnishow.omnigroup.com/")
let parsedFeed = try! FeedParser.parse(d)!
for article in parsedFeed.items {
XCTAssertNotNil(article.attachments)
XCTAssertEqual(article.attachments!.count, 1)
let attachment = Array(article.attachments!).first!
XCTAssertNotNil(attachment.mimeType)
XCTAssertNotNil(attachment.sizeInBytes)
XCTAssert(attachment.url.contains("cloudfront"))
XCTAssertGreaterThanOrEqual(attachment.sizeInBytes!, 22275279)
XCTAssertEqual(attachment.mimeType, "audio/mpeg")
}
}
} }

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0"><channel><title>The Omni Show</title><link>https://theomnishow.omnigroup.com/</link><atom:link href="https://theomnishow.omnigroup.com/rss/" rel="self" type="application/rss+xml" /><language>en-us</language><copyright>&#8471; &amp; &#169; 2017 The Omni Group</copyright><itunes:subtitle>Get to know the people and stories behind Omni&#8217;s award-winning productivity apps for Mac and iOS.</itunes:subtitle><itunes:author>The Omni Group</itunes:author><itunes:summary>Get to know the people and stories behind Omni&#8217;s award-winning productivity apps for Mac and iOS.</itunes:summary><description>Get to know the people and stories behind Omni&#8217;s award-winning productivity apps for Mac and iOS.</description><itunes:owner><itunes:name>The Omni Group</itunes:name><itunes:email>theomnishow@omnigroup.com</itunes:email></itunes:owner><itunes:image href="https://theomnishow.omnigroup.com/assets/img/omnishow-cover-1x3000.jpg" /><itunes:category text="Technology"><itunes:category text="Tech News" /></itunes:category><itunes:explicit>no</itunes:explicit><item><title>Andrea McVittie, User Experience Designer</title><itunes:author>The Omni Group</itunes:author><link>https://theomnishow.omnigroup.com/episode/andrea-mcvittie-user-experience-designer/</link><description>Andrea studied fine art at &lt;a href="https://msu.edu/"&gt;Michigan State&lt;/a&gt; and human-computer interaction at the &lt;a href="https://www.umich.edu/"&gt;University of Michigan&lt;/a&gt;. One day, while working at the school library, it was discovered that she could make websites &#8212; which launched her career as a UX designer, and since 2012 she&#8217;s been designing apps at Omni.</description><itunes:image href="https://theomnishow.omnigroup.com/assets/img/omnishow-cover-1x3000.jpg" /><enclosure length="24388318" type="audio/mpeg" url="https://d2vstsfo8knv0p.cloudfront.net/audio/the-omni-show-004.mp3" /><guid>https://theomnishow.omnigroup.com/episode/andrea-mcvittie-user-experience-designer/</guid><pubDate>Wed, 06 Dec 2017 14:00:00 +0000</pubDate><itunes:duration>28:49</itunes:duration><content:encoded>Full transcript and show notes at &lt;a href='https://theomnishow.omnigroup.com/episode/andrea-mcvittie-user-experience-designer/'&gt;theomnishow.omnigroup.com&lt;/a&gt;
&lt;p&gt;Andrea studied fine art at &lt;a href="https://msu.edu/"&gt;Michigan State&lt;/a&gt; and human-computer interaction at the &lt;a href="https://www.umich.edu/"&gt;University of Michigan&lt;/a&gt;. One day, while working at the school library, it was discovered that she could make websites &#8212; which launched her career as a UX designer, and since 2012 she&#8217;s been designing apps at Omni.&lt;/p&gt;
&lt;p&gt;Andrea, it must be told, is firmly against stealing money from puppies &#8212; or maybe she&#8217;s against treating puppies as a particularly cuddly form of currency and then stealing &lt;em&gt;them&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Nobody knows which. Don&#8217;t do either!&lt;/p&gt;
&lt;p&gt;You can find Andrea &lt;a href="https://twitter.com/amcvittie"&gt;@amcvittie&lt;/a&gt; on Twitter. And you can find her work in every single Omni app.&lt;/p&gt;
&lt;p&gt;Some other people, places, and things mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.nngroup.com/articles/definition-user-experience/"&gt;User Experience (UX)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://usabilitygeek.com/ethics-in-user-experience-design/"&gt;User Experience and Ethics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/monteiro"&gt;Mike Monteiro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/blog/omnifocus-now-supports-end-to-end-encryption"&gt;OmniFocus Sync Encryption&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/timothyekl"&gt;Tim Ekl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/omnigraffle"&gt;OmniGraffle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apple.com/apple-pencil/"&gt;Apple Pencil&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/blog/omni-roadmap-2017-q4-update"&gt;OmniFocus Tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://vesperapp.co/"&gt;Vesper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://theomnishow.omnigroup.com/episode/curt-clifton-omnifocus-engineer"&gt;Curt Clifton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/jimcorreia"&gt;Jim Correia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://darkpatterns.org/"&gt;darkpatterns.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.seattleartmuseum.org/"&gt;Seattle Art Museum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/OmniGroup/status/921132286527004672"&gt;Omni Slack Group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.rosemaryorchard.com/en"&gt;Rosemary Orchard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.omnigroup.com/"&gt;Omni Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/KelStewart84/status/928377769561546752"&gt;Listener question from Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lib.msu.edu/"&gt;Michigan State Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/Photoshop"&gt;Adobe Photoshop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://annarborchronicle.com/2012/05/03/in-the-archives-poison-pages/"&gt;Shadows from the Walls of Death&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.si.umich.edu/"&gt;University of Michigan School of Information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/p/Baztg5FF7wJ/"&gt;Oliver, who&#8217;s a good dog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><itunes:explicit>no</itunes:explicit></item><item><title>Brian Covey, Support Manager</title><itunes:author>The Omni Group</itunes:author><link>https://theomnishow.omnigroup.com/episode/brian-covey-support-manager/</link><description>Brian Covey studied computer science at &lt;a href="http://www.evergreen.edu/"&gt;The Evergreen State College&lt;/a&gt; &#8212; which your host also attended, but back before computers were a thing. After some normal young-person-bouncing-around a little, Brian came to shore at Omni in 2001, where he became the leader, dear and fearless, of Omni&#8217;s Support Humans.</description><itunes:image href="https://theomnishow.omnigroup.com/assets/img/omnishow-cover-1x3000.jpg" /><enclosure length="24985882" type="audio/mpeg" url="https://d2vstsfo8knv0p.cloudfront.net/audio/the-omni-show-003.mp3" /><guid>https://theomnishow.omnigroup.com/episode/brian-covey-support-manager/</guid><pubDate>Wed, 22 Nov 2017 14:00:00 +0000</pubDate><itunes:duration>29:32</itunes:duration><content:encoded>Full transcript and show notes at &lt;a href='https://theomnishow.omnigroup.com/episode/brian-covey-support-manager/'&gt;theomnishow.omnigroup.com&lt;/a&gt;
&lt;p&gt;Brian Covey studied computer science at &lt;a href="http://www.evergreen.edu/"&gt;The Evergreen State College&lt;/a&gt; &#8212; which your host also attended, but back before computers were a thing. After some normal young-person-bouncing-around a little, Brian came to shore at Omni in 2001, where he became the leader, dear and fearless, of Omni&#8217;s Support Humans.&lt;/p&gt;
&lt;p&gt;You can find Brian &lt;a href="https://twitter.com/hidvorak"&gt;@hidvorak&lt;/a&gt; on Twitter. (You can contact Omni support at &lt;a href="https://support.omnigroup.com/"&gt;support.omnigroup.com&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;You can also find a &lt;a href="https://www.omnigroup.com/blog/Yes_-_The_Omni_Video"&gt;video of Brian doing the worm&lt;/a&gt; in an Omni commercial from 2006. Absolutely not to be missed!&lt;/p&gt;
&lt;p&gt;Some other people, places, and things mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cherof.bandcamp.com/"&gt;The Music of Aaron Cherof&lt;/a&gt; (Aaron works at Omni and does the music for this show)&lt;/li&gt;
&lt;li&gt;&lt;a href="http://elevatesummit.co/"&gt;Elevate Support conference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/kcase"&gt;Ken Case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/chuckdude"&gt;Chuck Toporek&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/more"&gt;OmniWeb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www2.gov.bc.ca/"&gt;British Columbia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Ninja"&gt;Ninjas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://starwars.wikia.com/wiki/Droid"&gt;Droids&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/curtclifton"&gt;Curt Clifton&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Pair_programming"&gt;Pair programming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/omniplan"&gt;OmniPlan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Babylon_5"&gt;Babylon 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.seattle.gov/visiting-seattle"&gt;Seattle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.netzero.net/"&gt;NetZero&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/MacOS"&gt;Mac OS X&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Boomerang"&gt;Boomerangs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.nirvana.com/"&gt;Nirvana&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=o_hngjZUnOw"&gt;K Dorm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mopop.org/"&gt;Experience Music Project&lt;/a&gt; (now MoPOP)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Dave_Grohl"&gt;Dave Grohl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Nomeansno"&gt;Nomeansno&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Operant_conditioning_chamber"&gt;Skinner Box&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Mythical_Man-Month"&gt;Mythical Man Month&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Spokane,_Washington"&gt;Spokane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Anton%C3%ADn_Dvo&#345;&#225;k"&gt;Anton&#237;n Dvo&#345;&#225;k&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Dvorak_Simplified_Keyboard"&gt;Dvorak keyboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Repetitive_strain_injury"&gt;Repetitive Strain Injury (RSI)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><itunes:explicit>no</itunes:explicit></item><item><title>Curt Clifton, OmniFocus Engineer</title><itunes:author>The Omni Group</itunes:author><link>https://theomnishow.omnigroup.com/episode/curt-clifton-omnifocus-engineer/</link><description>Curt Clifton came to Omni while on sabbatical from his computer science professorship at &lt;a href="http://www.rose-hulman.edu/"&gt;Rose-Hulman&lt;/a&gt; &#8212; and never went back. He&#8217;s spent much of his six years at Omni on the &lt;a href="https://www.omnigroup.com/omnifocus"&gt;OmniFocus&lt;/a&gt; team, where he clicks checkboxes for a living.</description><itunes:image href="https://theomnishow.omnigroup.com/assets/img/omnishow-cover-1x3000.jpg" /><enclosure length="24354550" type="audio/mpeg" url="https://d2vstsfo8knv0p.cloudfront.net/audio/the-omni-show-002.mp3" /><guid>https://theomnishow.omnigroup.com/episode/curt-clifton-omnifocus-engineer/</guid><pubDate>Mon, 06 Nov 2017 17:00:00 +0000</pubDate><itunes:duration>28:47</itunes:duration><content:encoded>Full transcript and show notes at &lt;a href='https://theomnishow.omnigroup.com/episode/curt-clifton-omnifocus-engineer/'&gt;theomnishow.omnigroup.com&lt;/a&gt;
&lt;p&gt;Curt Clifton came to Omni while on sabbatical from his computer science professorship at &lt;a href="http://www.rose-hulman.edu/"&gt;Rose-Hulman&lt;/a&gt; &#8212; and never went back. He&#8217;s spent much of his six years at Omni on the &lt;a href="https://www.omnigroup.com/omnifocus"&gt;OmniFocus&lt;/a&gt; team, where he clicks checkboxes for a living.&lt;/p&gt;
&lt;p&gt;Curt&#8217;s a fan of the local &lt;a href="https://www.soundersfc.com/"&gt;association football team&lt;/a&gt;. He also likes woodworking and distance-running, though he has yet to explore the synergistic possibilities of doing both at the same time.&lt;/p&gt;
&lt;p&gt;You can find Curt &lt;a href="https://twitter.com/curtclifton"&gt;@curtclifton&lt;/a&gt; on Twitter and at &lt;a href="http://curtclifton.net/"&gt;curtclifton.net&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some other people, places, and things mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/ios/drag-and-drop/"&gt;iOS 11 drag and drop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/blog/omni-roadmap-2017-q4-update"&gt;OmniFocus 3: tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.apple.com/en-us/HT206481"&gt;Files app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/wwdc/"&gt;World Wide Developer&#8217;s Conference (WWDC) (aka Dub Dub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Lake_Union"&gt;Lake Union&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arstechnica.com/gadgets/2013/09/ios-7-thoroughly-reviewed/"&gt;iOS 7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.apple.com/iphone-x/"&gt;The notch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://macosxautomation.com/applescript/"&gt;AppleScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/omnioutliner"&gt;OmniOutliner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://ethanschoonover.com/"&gt;Ethan Schoonover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://shawnblanc.net/2014/03/history-of-omnifocus-for-mac/"&gt;Kinkless GTD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://gettingthingsdone.com/"&gt;Getting Things Done, by David Allen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Indiana"&gt;Indiana&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discourse.omnigroup.com/"&gt;Omni Forums&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/kcase"&gt;Ken Case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/tjw"&gt;Tim Wood&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Amish"&gt;The Amish&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/vanillaice"&gt;Vanilla Ice&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.imdb.com/title/tt0090555/"&gt;Crocodile Dundee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Pennsylvania_Dutch_Country"&gt;Pennsylvania Dutch Country&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.goodnplenty.com"&gt;Good &#8217;N Plenty Restaurant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Amana_Colonies"&gt;Amana Colonies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/timothyekl"&gt;Tim Ekl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/jimcorreia"&gt;Jim Correia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pomona.edu/"&gt;Pomona College&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hmc.edu/"&gt;Harvey Mudd College&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/reid_callan"&gt;Reid Callan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/tbunch"&gt;Tom Bunch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlb.com/cubs"&gt;Cubs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlssoccer.com/"&gt;Major League Soccer Cup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.baa.org/races/boston-marathon.aspx"&gt;Boston Marathon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Battle_of_Marathon"&gt;Marathon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.omnigroup.com/"&gt;Support Humans&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><itunes:explicit>no</itunes:explicit></item><item><title>Kristina Sontag, Software Test Manager</title><itunes:author>The Omni Group</itunes:author><link>https://theomnishow.omnigroup.com/episode/kristina-sontag-software-test-manager/</link><description>For our first episode we interview Kristina Sontag, who runs Omni&#8217;s test department &#8212; a group tireless, fearless, and strikingly effective in its hunt for bugs.</description><itunes:image href="https://theomnishow.omnigroup.com/assets/img/omnishow-cover-1x3000.jpg" /><enclosure length="22275279" type="audio/mpeg" url="https://d2vstsfo8knv0p.cloudfront.net/audio/the-omni-show-001.mp3" /><guid>https://theomnishow.omnigroup.com/episode/kristina-sontag-software-test-manager/</guid><pubDate>Wed, 01 Nov 2017 16:00:00 +0000</pubDate><itunes:duration>26:18</itunes:duration><content:encoded>Full transcript and show notes at &lt;a href='https://theomnishow.omnigroup.com/episode/kristina-sontag-software-test-manager/'&gt;theomnishow.omnigroup.com&lt;/a&gt;
&lt;p&gt;For our first episode we interview Kristina Sontag, who runs Omni&#8217;s test department &#8212; a group tireless, fearless, and strikingly effective in its hunt for bugs.&lt;/p&gt;
&lt;p&gt;Kristina got her start playing games for a living at &lt;a href="http://lucasfilm.com/games"&gt;LucasArts&lt;/a&gt;. Between there and today she&#8217;s also worked on &lt;a href="https://web.archive.org/web/19970105135152/http://www.edmark.com/"&gt;educational software&lt;/a&gt;, gotten a pilot&#8217;s license, volunteered for &lt;a href="http://appcamp4girls.com/"&gt;App Camp for Girls&lt;/a&gt;, and helped organize the &lt;a href="https://www.weareecs.com/"&gt;Emerald City Supporters&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When she&#8217;s not doing everything else, she&#8217;s playing &lt;a href="https://www.destinythegame.com/"&gt;Destiny&lt;/a&gt;. She may even be thinking about Destiny at this very moment.&lt;/p&gt;
&lt;p&gt;You can find Kristina on Twitter &lt;a href="https://twitter.com/knsontag"&gt;@knsontag&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some other people and places mentioned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/wwdc/"&gt;WWDC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/macgenie"&gt;Jean McDonald&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/emarley"&gt;Liz Marley&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/uipoptart"&gt;Grey Osten&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://itunes.apple.com/us/developer/app-camp-for-girls/id942207153"&gt;App Camp App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Santa_Rosa,_California"&gt;Santa Rosa, CA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.ilm.com/"&gt;Industrial Light &amp;amp; Magic (ILM)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://lucasfilm.com/skywalker-ranch-marin"&gt;Skywalker Ranch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/George_Lucas"&gt;George Lucas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Ron_Gilbert"&gt;Ron Gilbert&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://humongous.com/"&gt;Humongous Entertainment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.omnigroup.com/omnifocus"&gt;OmniFocus for iPhone&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/hidvorak"&gt;Brian Covey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/kcase"&gt;Ken Case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Floatplane"&gt;Float plane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kenmoreair.com/"&gt;Kenmore Air&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.soundersfc.com/"&gt;Seattle Sounders&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mlssoccer.com/"&gt;Major League Soccer (MLS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><itunes:explicit>no</itunes:explicit></item></channel></rss>