From d6f4906b73b961c8d1edab884a16255c9cc5e3b2 Mon Sep 17 00:00:00 2001 From: magodo Date: Sun, 11 Aug 2019 14:35:24 +0800 Subject: [PATCH] RSS feed add element with "itunes:" prefix (for podcast) --- lib/domain/rss_feed.dart | 5 ++ lib/domain/rss_item.dart | 5 ++ lib/domain/rss_item_itunes.dart | 76 +++++++++++++++++++++++++++++ lib/domain/rss_itunes.dart | 65 ++++++++++++++++++++++++ lib/domain/rss_itunes_category.dart | 24 +++++++++ lib/domain/rss_itunes_image.dart | 14 ++++++ lib/util/helpers.dart | 4 +- test/rss_test.dart | 27 ++++++++++ test/xml/RSS-Itunes.xml | 44 +++++++++++++++++ 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 lib/domain/rss_item_itunes.dart create mode 100644 lib/domain/rss_itunes.dart create mode 100644 lib/domain/rss_itunes_category.dart create mode 100644 lib/domain/rss_itunes_image.dart create mode 100644 test/xml/RSS-Itunes.xml diff --git a/lib/domain/rss_feed.dart b/lib/domain/rss_feed.dart index b1ef0c0..6978ef1 100644 --- a/lib/domain/rss_feed.dart +++ b/lib/domain/rss_feed.dart @@ -8,6 +8,8 @@ import 'package:webfeed/domain/rss_item.dart'; import 'package:webfeed/util/helpers.dart'; import 'package:xml/xml.dart'; +import 'rss_itunes.dart'; + class RssFeed { final String title; final String author; @@ -30,6 +32,7 @@ class RssFeed { final String webMaster; final int ttl; final DublinCore dc; + final RssItunes itunes; RssFeed({ this.title, @@ -52,6 +55,7 @@ class RssFeed { this.webMaster, this.ttl, this.dc, + this.itunes, }); factory RssFeed.parse(String xmlString) { @@ -98,6 +102,7 @@ class RssFeed { webMaster: findElementOrNull(channelElement, "webMaster")?.text, ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"), dc: DublinCore.parse(channelElement), + itunes: RssItunes.parse(channelElement), ); } } diff --git a/lib/domain/rss_item.dart b/lib/domain/rss_item.dart index 4f64d67..54c1d5c 100644 --- a/lib/domain/rss_item.dart +++ b/lib/domain/rss_item.dart @@ -7,6 +7,8 @@ import 'package:webfeed/domain/rss_source.dart'; import 'package:webfeed/util/helpers.dart'; import 'package:xml/xml.dart'; +import 'rss_item_itunes.dart'; + class RssItem { final String title; final String description; @@ -22,6 +24,7 @@ class RssItem { final Media media; final RssEnclosure enclosure; final DublinCore dc; + final RssItemItunes itunes; RssItem({ this.title, @@ -37,6 +40,7 @@ class RssItem { this.media, this.enclosure, this.dc, + this.itunes, }); factory RssItem.parse(XmlElement element) { @@ -56,6 +60,7 @@ class RssItem { media: Media.parse(element), enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), dc: DublinCore.parse(element), + itunes: RssItemItunes.parse(element), ); } } diff --git a/lib/domain/rss_item_itunes.dart b/lib/domain/rss_item_itunes.dart new file mode 100644 index 0000000..ce3d251 --- /dev/null +++ b/lib/domain/rss_item_itunes.dart @@ -0,0 +1,76 @@ +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +import 'rss_itunes_category.dart'; +import 'rss_itunes_image.dart'; + +class RssItemItunes { + final int episode; + final Duration duration; + final String episodeType; + final String author; + final String summary; + final bool explicit; + final String subtitle; + final List keywords; + final RssItunesImage image; + final RssItunesCategory category; + + RssItemItunes({ + this.episode, + this.duration, + this.episodeType, + this.author, + this.summary, + this.explicit, + this.subtitle, + this.keywords, + this.image, + this.category, + }); + + factory RssItemItunes.parse(XmlElement element) { + if (element == null) { + return null; + } + var explicitStr = + findElementOrNull(element, "itunes:explicit")?.text?.toLowerCase()?.trim(); + var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim(); + var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim(); + + return RssItemItunes( + episode: episodeStr == null ? null : int.parse(episodeStr), + duration: durationStr == null ? null : parseDuration(durationStr), + episodeType: findElementOrNull(element, "itunes:episodeType")?.text?.trim(), + author: findElementOrNull(element, "itunes:author")?.text?.trim(), + summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), + explicit: explicitStr == null + ? null + : explicitStr == "yes" || explicitStr == "true", + subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), + image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), + category: RssItunesCategory.parse( + findElementOrNull(element, "itunes:category")), + ); + } +} + +Duration parseDuration(String s) { + var hours = 0; + var minutes = 0; + var seconds = 0; + var parts = s.split(':'); + if (parts.length > 2) { + hours = int.parse(parts[parts.length - 3]); + } + if (parts.length > 1) { + minutes = int.parse(parts[parts.length - 2]); + } + seconds = int.parse(parts[parts.length - 1]); + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + ); +} diff --git a/lib/domain/rss_itunes.dart b/lib/domain/rss_itunes.dart new file mode 100644 index 0000000..e29a51a --- /dev/null +++ b/lib/domain/rss_itunes.dart @@ -0,0 +1,65 @@ +import 'package:xml/xml.dart'; + +import 'package:webfeed/util/helpers.dart'; + +import 'rss_itunes_category.dart'; +import 'rss_itunes_image.dart'; + +class RssItunes { + final String author; + final String summary; + final bool explicit; + final String subtitle; + final RssItunesOwner owner; + final List keywords; + final RssItunesImage image; + final RssItunesCategory category; + + RssItunes({ + this.author, + this.summary, + this.explicit, + this.subtitle, + this.owner, + this.keywords, + this.image, + this.category, + }); + + factory RssItunes.parse(XmlElement element) { + if (element == null) { + return null; + } + var explicitStr = + findElementOrNull(element, "itunes:explicit")?.text?.toLowerCase()?.trim(); + + return RssItunes( + author: findElementOrNull(element, "itunes:author")?.text?.trim(), + summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), + explicit: explicitStr == null + ? null + : explicitStr == "yes" || explicitStr == "true", + subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")), + keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), + image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), + category: RssItunesCategory.parse( + findElementOrNull(element, "itunes:category")), + ); + } +} + +class RssItunesOwner { + final String name; + final String email; + + RssItunesOwner({this.name, this.email}); + + factory RssItunesOwner.parse(XmlElement element) { + if (element == null) return null; + return RssItunesOwner( + name: findElementOrNull(element, "itunes:name")?.text?.trim(), + email: findElementOrNull(element, "itunes:email")?.text?.trim(), + ); + } +} diff --git a/lib/domain/rss_itunes_category.dart b/lib/domain/rss_itunes_category.dart new file mode 100644 index 0000000..290d2f2 --- /dev/null +++ b/lib/domain/rss_itunes_category.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class RssItunesCategory { + final String category; + final List subCategories; + + RssItunesCategory({this.category, this.subCategories}); + + factory RssItunesCategory.parse(XmlElement element) { + if (element == null) return null; + + Iterable subCategories; + try { + subCategories = element.findAllElements("itunes:category"); + } on StateError { + subCategories = null; + } + return RssItunesCategory( + category: element.getAttribute("text")?.trim(), + subCategories: + subCategories?.map((ele) => ele.getAttribute("text")?.trim())?.toList(), + ); + } +} diff --git a/lib/domain/rss_itunes_image.dart b/lib/domain/rss_itunes_image.dart new file mode 100644 index 0000000..7b2ef2f --- /dev/null +++ b/lib/domain/rss_itunes_image.dart @@ -0,0 +1,14 @@ +import 'package:xml/xml.dart'; + +class RssItunesImage { + final String href; + + RssItunesImage({this.href}); + + factory RssItunesImage.parse(XmlElement element) { + if (element == null) return null; + return RssItunesImage( + href: element.getAttribute("href")?.trim(), + ); + } +} diff --git a/lib/util/helpers.dart b/lib/util/helpers.dart index fdce64c..b2fe8a1 100644 --- a/lib/util/helpers.dart +++ b/lib/util/helpers.dart @@ -2,9 +2,9 @@ import 'dart:core'; import 'package:xml/xml.dart'; -XmlElement findElementOrNull(XmlElement element, String name) { +XmlElement findElementOrNull(XmlElement element, String name, {String namespace}) { try { - return element.findAllElements(name).first; + return element.findAllElements(name, namespace: namespace).first; } on StateError { return null; } diff --git a/test/rss_test.dart b/test/rss_test.dart index 2ec09b9..23ab088 100644 --- a/test/rss_test.dart +++ b/test/rss_test.dart @@ -295,4 +295,31 @@ void main() { expect(feed.items.first.content, null); }); + + test("parse RSS-Itunes.xml", () { + var xmlString = File("test/xml/RSS-Itunes.xml").readAsStringSync(); + + var feed = RssFeed.parse(xmlString); + + expect(feed.itunes.author, "Changelog Media"); + expect(feed.itunes.summary, "Foo"); + expect(feed.itunes.explicit, false); + expect(feed.itunes.image.href, "https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"); + expect(feed.itunes.keywords, "go,golang,open source,software,development".split(",")); + expect(feed.itunes.owner.name, "Changelog Media"); + expect(feed.itunes.owner.email, "editors@changelog.com"); + expect(feed.itunes.category.category, "Technology"); + expect(feed.itunes.category.subCategories, ["Software How-To", "Tech News"]); + + var item = feed.items[0]; + expect(item.itunes.episodeType, "full"); + expect(item.itunes.episode, 1); + expect(item.itunes.image.href, "https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"); + expect(item.itunes.duration, Duration(minutes: 32, seconds: 30)); + expect(item.itunes.explicit, false); + expect(item.itunes.keywords, "go,golang,open source,software,development".split(",")); + expect(item.itunes.subtitle, "with Erik, Carlisia, and Brian"); + expect(item.itunes.summary, "Foo"); + expect(item.itunes.author, "Erik St. Martin, Carlisia Pinto, and Brian Ketelsen"); + }); } diff --git a/test/xml/RSS-Itunes.xml b/test/xml/RSS-Itunes.xml new file mode 100644 index 0000000..490da01 --- /dev/null +++ b/test/xml/RSS-Itunes.xml @@ -0,0 +1,44 @@ + + + Go Time + All rights reserved + https://changelog.com/gotime + + + + + en-us + + A diverse panel and special guests discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker…oh and also Go! This show records LIVE every Thursday at 3pm US Eastern. A diverse panel and special guests discuss cloud infrastructure, distributed systems, microservices, Kubernetes, Docker…oh and also Go! + + Changelog Media + Foo + no + + go, golang, open source, software, development + + Changelog Media + editors@changelog.com + + + + + + + full + 1 + + 32:30 + no + go, golang, open source, software, development + with Erik, Carlisia, and Brian + Foo + + Erik St. Martin, Carlisia Pinto, and Brian Ketelsen + + + Erik St. Martin, Carlisia Pinto, and Brian Ketelsen + + + + \ No newline at end of file