| @@ -8,6 +8,8 @@ import 'package:webfeed/domain/rss_item.dart'; | |||||
| import 'package:webfeed/util/helpers.dart'; | import 'package:webfeed/util/helpers.dart'; | ||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| import 'rss_itunes.dart'; | |||||
| class RssFeed { | class RssFeed { | ||||
| final String title; | final String title; | ||||
| final String author; | final String author; | ||||
| @@ -30,6 +32,7 @@ class RssFeed { | |||||
| final String webMaster; | final String webMaster; | ||||
| final int ttl; | final int ttl; | ||||
| final DublinCore dc; | final DublinCore dc; | ||||
| final RssItunes itunes; | |||||
| RssFeed({ | RssFeed({ | ||||
| this.title, | this.title, | ||||
| @@ -52,6 +55,7 @@ class RssFeed { | |||||
| this.webMaster, | this.webMaster, | ||||
| this.ttl, | this.ttl, | ||||
| this.dc, | this.dc, | ||||
| this.itunes, | |||||
| }); | }); | ||||
| factory RssFeed.parse(String xmlString) { | factory RssFeed.parse(String xmlString) { | ||||
| @@ -98,6 +102,7 @@ class RssFeed { | |||||
| webMaster: findElementOrNull(channelElement, "webMaster")?.text, | webMaster: findElementOrNull(channelElement, "webMaster")?.text, | ||||
| ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"), | ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"), | ||||
| dc: DublinCore.parse(channelElement), | dc: DublinCore.parse(channelElement), | ||||
| itunes: RssItunes.parse(channelElement), | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -7,6 +7,8 @@ import 'package:webfeed/domain/rss_source.dart'; | |||||
| import 'package:webfeed/util/helpers.dart'; | import 'package:webfeed/util/helpers.dart'; | ||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| import 'rss_item_itunes.dart'; | |||||
| class RssItem { | class RssItem { | ||||
| final String title; | final String title; | ||||
| final String description; | final String description; | ||||
| @@ -22,6 +24,7 @@ class RssItem { | |||||
| final Media media; | final Media media; | ||||
| final RssEnclosure enclosure; | final RssEnclosure enclosure; | ||||
| final DublinCore dc; | final DublinCore dc; | ||||
| final RssItemItunes itunes; | |||||
| RssItem({ | RssItem({ | ||||
| this.title, | this.title, | ||||
| @@ -37,6 +40,7 @@ class RssItem { | |||||
| this.media, | this.media, | ||||
| this.enclosure, | this.enclosure, | ||||
| this.dc, | this.dc, | ||||
| this.itunes, | |||||
| }); | }); | ||||
| factory RssItem.parse(XmlElement element) { | factory RssItem.parse(XmlElement element) { | ||||
| @@ -56,6 +60,7 @@ class RssItem { | |||||
| media: Media.parse(element), | media: Media.parse(element), | ||||
| enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), | enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), | ||||
| dc: DublinCore.parse(element), | dc: DublinCore.parse(element), | ||||
| itunes: RssItemItunes.parse(element), | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -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<String> 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, | |||||
| ); | |||||
| } | |||||
| @@ -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<String> 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(), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| import 'package:xml/xml.dart'; | |||||
| class RssItunesCategory { | |||||
| final String category; | |||||
| final List<String> subCategories; | |||||
| RssItunesCategory({this.category, this.subCategories}); | |||||
| factory RssItunesCategory.parse(XmlElement element) { | |||||
| if (element == null) return null; | |||||
| Iterable<XmlElement> 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(), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -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(), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -2,9 +2,9 @@ import 'dart:core'; | |||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| XmlElement findElementOrNull(XmlElement element, String name) { | |||||
| XmlElement findElementOrNull(XmlElement element, String name, {String namespace}) { | |||||
| try { | try { | ||||
| return element.findAllElements(name).first; | |||||
| return element.findAllElements(name, namespace: namespace).first; | |||||
| } on StateError { | } on StateError { | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -295,4 +295,31 @@ void main() { | |||||
| expect(feed.items.first.content, null); | 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"); | |||||
| }); | |||||
| } | } | ||||
| @@ -0,0 +1,44 @@ | |||||
| <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0" nighteye="disabled"> | |||||
| <channel> | |||||
| <title>Go Time</title> | |||||
| <copyright>All rights reserved</copyright> | |||||
| <link>https://changelog.com/gotime</link> | |||||
| <atom:link href="https://changelog.com/gotime/feed" rel="self" type="application/rss+xml"/> | |||||
| <atom:link href="https://changelog.com/gotime/feed" rel="first" type="application/rss+xml"/> | |||||
| <atom:link href="https://changelog.com/gotime/feed?page=1" rel="last" type="application/rss+xml"/> | |||||
| <atom:link href="https://changelog.com/gotime" rel="alternate" type="text/html"/> | |||||
| <language>en-us</language> | |||||
| <description> | |||||
| 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! | |||||
| </description> | |||||
| <itunes:author>Changelog Media</itunes:author> | |||||
| <itunes:summary>Foo</itunes:summary> | |||||
| <itunes:explicit>no</itunes:explicit> | |||||
| <itunes:image href="https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"/> | |||||
| <itunes:keywords>go, golang, open source, software, development</itunes:keywords> | |||||
| <itunes:owner> | |||||
| <itunes:name>Changelog Media</itunes:name> | |||||
| <itunes:email>editors@changelog.com</itunes:email> | |||||
| </itunes:owner> | |||||
| <itunes:category text="Technology"> | |||||
| <itunes:category text="Software How-To"/> | |||||
| <itunes:category text="Tech News"/> | |||||
| </itunes:category> | |||||
| <item> | |||||
| <itunes:episodeType>full</itunes:episodeType> | |||||
| <itunes:episode>1</itunes:episode> | |||||
| <itunes:image href="https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357"/> | |||||
| <itunes:duration>32:30</itunes:duration> | |||||
| <itunes:explicit>no</itunes:explicit> | |||||
| <itunes:keywords>go, golang, open source, software, development</itunes:keywords> | |||||
| <itunes:subtitle>with Erik, Carlisia, and Brian</itunes:subtitle> | |||||
| <itunes:summary>Foo</itunes:summary> | |||||
| <dc:creator> | |||||
| Erik St. Martin, Carlisia Pinto, and Brian Ketelsen | |||||
| </dc:creator> | |||||
| <itunes:author> | |||||
| Erik St. Martin, Carlisia Pinto, and Brian Ketelsen | |||||
| </itunes:author> | |||||
| </item> | |||||
| </channel> | |||||
| </rss> | |||||