From 67f38f898c5e5db02fea7b5acf2ec208c54c3673 Mon Sep 17 00:00:00 2001 From: magodo Date: Thu, 15 Aug 2019 22:22:31 +0800 Subject: [PATCH] complement itunes tag per [official recommendation](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) --- lib/domain/rss_item_itunes.dart | 21 ++++-- lib/domain/rss_itunes.dart | 54 ++++++++------- lib/domain/rss_itunes_category.dart | 2 +- lib/domain/rss_itunes_episode_type.dart | 19 +++++ lib/domain/rss_itunes_owner.dart | 18 +++++ lib/domain/rss_itunes_type.dart | 17 +++++ lib/util/helpers.dart | 19 ++++- test/rss_test.dart | 92 ++++++++++++++++++------- test/xml/RSS-Itunes.xml | 12 ++++ 9 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 lib/domain/rss_itunes_episode_type.dart create mode 100644 lib/domain/rss_itunes_owner.dart create mode 100644 lib/domain/rss_itunes_type.dart diff --git a/lib/domain/rss_item_itunes.dart b/lib/domain/rss_item_itunes.dart index ce3d251..3d89cc5 100644 --- a/lib/domain/rss_item_itunes.dart +++ b/lib/domain/rss_item_itunes.dart @@ -2,12 +2,15 @@ import 'package:webfeed/util/helpers.dart'; import 'package:xml/xml.dart'; import 'rss_itunes_category.dart'; +import 'rss_itunes_episode_type.dart'; import 'rss_itunes_image.dart'; class RssItemItunes { + final String title; final int episode; + final int season; final Duration duration; - final String episodeType; + final RssItunesEpisodeType episodeType; final String author; final String summary; final bool explicit; @@ -15,9 +18,12 @@ class RssItemItunes { final List keywords; final RssItunesImage image; final RssItunesCategory category; + final bool block; RssItemItunes({ + this.title, this.episode, + this.season, this.duration, this.episodeType, this.author, @@ -27,31 +33,32 @@ class RssItemItunes { this.keywords, this.image, this.category, + this.block, }); 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 seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim(); var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim(); return RssItemItunes( + title: findElementOrNull(element, "itunes:title")?.text?.trim(), episode: episodeStr == null ? null : int.parse(episodeStr), + season: seasonStr == null ? null : int.parse(seasonStr), duration: durationStr == null ? null : parseDuration(durationStr), - episodeType: findElementOrNull(element, "itunes:episodeType")?.text?.trim(), + episodeType: newRssItunesEpisodeType(findElementOrNull(element, "itunes:episodeType")), author: findElementOrNull(element, "itunes:author")?.text?.trim(), summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), - explicit: explicitStr == null - ? null - : explicitStr == "yes" || explicitStr == "true", + explicit: parseBoolLiteral(element, "itunes:explicit"), 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")), + block: parseBoolLiteral(element, "itunes:block"), ); } } diff --git a/lib/domain/rss_itunes.dart b/lib/domain/rss_itunes.dart index e29a51a..c58ad66 100644 --- a/lib/domain/rss_itunes.dart +++ b/lib/domain/rss_itunes.dart @@ -4,62 +4,66 @@ import 'package:webfeed/util/helpers.dart'; import 'rss_itunes_category.dart'; import 'rss_itunes_image.dart'; +import 'rss_itunes_owner.dart'; +import 'rss_itunes_type.dart'; class RssItunes { final String author; final String summary; final bool explicit; + final String title; final String subtitle; final RssItunesOwner owner; final List keywords; final RssItunesImage image; - final RssItunesCategory category; + final List categories; + final RssItunesType type; + final String newFeedUrl; + final bool block; + final bool complete; RssItunes({ this.author, this.summary, this.explicit, + this.title, this.subtitle, this.owner, this.keywords, this.image, - this.category, + this.categories, + this.type, + this.newFeedUrl, + this.block, + this.complete, }); 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", + explicit: parseBoolLiteral(element, "itunes:explicit"), + title: findElementOrNull(element, "itunes:title")?.text?.trim(), 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(), + 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")), + categories: findAllDirectElementsOrNull(element, "itunes:category") + .map((ele) => RssItunesCategory.parse(ele)) + .toList(), + type: newRssItunesType(findElementOrNull(element, "itunes:type")), + newFeedUrl: + findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(), + block: parseBoolLiteral(element, "itunes:block"), + complete: parseBoolLiteral(element, "itunes:complete"), ); } } -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 index 290d2f2..b79f339 100644 --- a/lib/domain/rss_itunes_category.dart +++ b/lib/domain/rss_itunes_category.dart @@ -11,7 +11,7 @@ class RssItunesCategory { Iterable subCategories; try { - subCategories = element.findAllElements("itunes:category"); + subCategories = element.findElements("itunes:category"); } on StateError { subCategories = null; } diff --git a/lib/domain/rss_itunes_episode_type.dart b/lib/domain/rss_itunes_episode_type.dart new file mode 100644 index 0000000..30e639d --- /dev/null +++ b/lib/domain/rss_itunes_episode_type.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +enum RssItunesEpisodeType {full, trailer, bonus} + +RssItunesEpisodeType newRssItunesEpisodeType(XmlElement element) { + // "full" is default type + if (element == null) return RssItunesEpisodeType.full; + + switch (element.text) { + case "full": + return RssItunesEpisodeType.full; + case "trailer": + return RssItunesEpisodeType.trailer; + case "bonus": + return RssItunesEpisodeType.bonus; + default: + return null; + } +} diff --git a/lib/domain/rss_itunes_owner.dart b/lib/domain/rss_itunes_owner.dart new file mode 100644 index 0000000..4bcadc5 --- /dev/null +++ b/lib/domain/rss_itunes_owner.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +import '../util/helpers.dart'; + +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_type.dart b/lib/domain/rss_itunes_type.dart new file mode 100644 index 0000000..b2cb031 --- /dev/null +++ b/lib/domain/rss_itunes_type.dart @@ -0,0 +1,17 @@ +import 'package:xml/xml.dart'; + +enum RssItunesType { episodic, serial } + +RssItunesType newRssItunesType(XmlElement element) { + // "episodic" is default type + if (element == null) return RssItunesType.episodic; + + switch (element.text) { + case "episodic": + return RssItunesType.episodic; + case "serial": + return RssItunesType.serial; + default: + return null; + } +} diff --git a/lib/util/helpers.dart b/lib/util/helpers.dart index b2fe8a1..179926c 100644 --- a/lib/util/helpers.dart +++ b/lib/util/helpers.dart @@ -2,10 +2,27 @@ import 'dart:core'; import 'package:xml/xml.dart'; -XmlElement findElementOrNull(XmlElement element, String name, {String namespace}) { +XmlElement findElementOrNull(XmlElement element, String name, + {String namespace}) { try { return element.findAllElements(name, namespace: namespace).first; } on StateError { return null; } } + +List findAllDirectElementsOrNull(XmlElement element, String name, + {String namespace}) { + try { + return element.findElements(name, namespace: namespace).toList(); + } on StateError { + return null; + } +} + +bool parseBoolLiteral(XmlElement element, String tagName) { + var v = findElementOrNull(element, tagName)?.text?.toLowerCase()?.trim(); + if (v == null) return null; + return ["yes", "true"].contains(v); +} + diff --git a/test/rss_test.dart b/test/rss_test.dart index 23ab088..5b14dfc 100644 --- a/test/rss_test.dart +++ b/test/rss_test.dart @@ -4,6 +4,9 @@ import 'dart:io'; import 'package:test/test.dart'; import 'package:webfeed/webfeed.dart'; +import 'package:webfeed/domain/rss_itunes_type.dart'; +import 'package:webfeed/domain/rss_itunes_episode_type.dart'; + void main() { test("parse Invalid.xml", () { var xmlString = new File("test/xml/Invalid.xml").readAsStringSync(); @@ -19,7 +22,8 @@ void main() { var feed = new RssFeed.parse(xmlString); expect(feed.title, "News - Foo bar News"); - expect(feed.description, "Foo bar News and Updates feed provided by Foo bar, Inc."); + expect(feed.description, + "Foo bar News and Updates feed provided by Foo bar, Inc."); expect(feed.link, "https://foo.bar.news/"); expect(feed.author, "hello@world.net"); expect(feed.language, "en-US"); @@ -62,8 +66,10 @@ void main() { expect(feed.items.length, 2); - expect(feed.items.first.title, "The standard Lorem Ipsum passage, used since the 1500s"); - expect(feed.items.first.description, "Lorem ipsum dolor sit amet, consectetur adipiscing elit"); + expect(feed.items.first.title, + "The standard Lorem Ipsum passage, used since the 1500s"); + expect(feed.items.first.description, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit"); expect(feed.items.first.link, "https://foo.bar.news/1"); expect(feed.items.first.guid, "https://foo.bar.news/1?guid"); expect(feed.items.first.pubDate, "Mon, 26 Mar 2018 14:00:00 PDT"); @@ -73,22 +79,26 @@ void main() { expect(feed.items.first.source.url, "https://foo.bar.news/1?source"); expect(feed.items.first.source.value, "Foo Bar"); expect(feed.items.first.comments, "https://foo.bar.news/1/comments"); - expect(feed.items.first.enclosure.url, "http://www.scripting.com/mp3s/weatherReportSuite.mp3"); + expect(feed.items.first.enclosure.url, + "http://www.scripting.com/mp3s/weatherReportSuite.mp3"); expect(feed.items.first.enclosure.length, 12216320); expect(feed.items.first.enclosure.type, "audio/mpeg"); - expect(feed.items.first.content.value, " Test content
"); - expect(feed.items.first.content.images.first, "https://test.com/image_link"); + expect(feed.items.first.content.value, + " Test content
"); + expect( + feed.items.first.content.images.first, "https://test.com/image_link"); }); - test("parse RSS-Media.xml", (){ + test("parse RSS-Media.xml", () { var xmlString = new File("test/xml/RSS-Media.xml").readAsStringSync(); var feed = new RssFeed.parse(xmlString); expect(feed.title, "Song Site"); - expect(feed.description, "Media RSS example with new fields added in v1.5.0"); + expect( + feed.description, "Media RSS example with new fields added in v1.5.0"); expect(feed.items.length, 1); - + var item = feed.items.first; expect(item.title, null); expect(item.link, "http://www.foo.com"); @@ -118,7 +128,8 @@ void main() { expect(mediaCredit.scheme, "urn:yvs"); expect(mediaCredit.value, "copyright holder of the entity"); - expect(item.media.category.scheme, "http://search.yahoo.com/mrss/category_ schema"); + expect(item.media.category.scheme, + "http://search.yahoo.com/mrss/category_ schema"); expect(item.media.category.label, "Music"); expect(item.media.category.value, "music/artist/album/song"); @@ -129,10 +140,11 @@ void main() { expect(item.media.title.value, "The Judy's -- The Moo Song"); expect(item.media.description.type, "plain"); - expect(item.media.description.value, "This was some really bizarre band I listened to as a young lad."); - + expect(item.media.description.value, + "This was some really bizarre band I listened to as a young lad."); + expect(item.media.keywords, "kitty, cat, big dog, yarn, fluffy"); - + expect(item.media.thumbnails.length, 2); var mediaThumbnail = item.media.thumbnails.first; expect(mediaThumbnail.url, "http://www.foo.com/keyframe1.jpg"); @@ -179,7 +191,8 @@ void main() { expect(item.media.embed.height, 323); expect(item.media.embed.params.length, 5); expect(item.media.embed.params.first.name, "type"); - expect(item.media.embed.params.first.value, "application/x-shockwave-flash"); + expect( + item.media.embed.params.first.value, "application/x-shockwave-flash"); expect(item.media.responses.length, 2); expect(item.media.responses.first, "http://www.response1.com"); @@ -195,7 +208,8 @@ void main() { expect(item.media.prices.length, 2); expect(item.media.prices.first.price, 19.99); expect(item.media.prices.first.type, "rent"); - expect(item.media.prices.first.info, "http://www.dummy.jp/package_info.html"); + expect( + item.media.prices.first.info, "http://www.dummy.jp/package_info.html"); expect(item.media.prices.first.currency, "EUR"); expect(item.media.license.type, "text/html"); @@ -214,7 +228,7 @@ void main() { expect(item.media.scenes.first.startTime, "00:15"); expect(item.media.scenes.first.endTime, "00:45"); }); - test("parse RSS-DC.xml", (){ + test("parse RSS-DC.xml", () { var xmlString = File("test/xml/RSS-DC.xml").readAsStringSync(); var feed = RssFeed.parse(xmlString); @@ -256,7 +270,7 @@ void main() { var xmlString = File("test/xml/RSS-Empty.xml").readAsStringSync(); var feed = RssFeed.parse(xmlString); - + expect(feed.title, null); expect(feed.description, null); expect(feed.link, null); @@ -304,22 +318,50 @@ void main() { 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.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"]); + expect( + Set.from([ + feed.itunes.categories[0].category, + feed.itunes.categories[1].category + ]), + ["Technology", "Foo"]); + for (var category in feed.itunes.categories) { + switch (category.category) { + case "Foo": + expect(category.subCategories, ["Bar", "Baz"]); + break; + case "Technology": + expect(category.subCategories, ["Software How-To", "Tech News"]); + break; + } + } + expect(feed.itunes.title, "Go Time"); + expect(feed.itunes.type, RssItunesType.serial); + expect(feed.itunes.newFeedUrl, "wubawuba"); + expect(feed.itunes.block, true); + expect(feed.itunes.complete, true); var item = feed.items[0]; - expect(item.itunes.episodeType, "full"); + expect(item.itunes.episodeType, RssItunesEpisodeType.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.season, 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.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"); + expect(item.itunes.author, + "Erik St. Martin, Carlisia Pinto, and Brian Ketelsen"); + expect(item.itunes.explicit, false); + expect(item.itunes.title, "awesome title"); + expect(item.itunes.block, false); }); } diff --git a/test/xml/RSS-Itunes.xml b/test/xml/RSS-Itunes.xml index 490da01..237b564 100644 --- a/test/xml/RSS-Itunes.xml +++ b/test/xml/RSS-Itunes.xml @@ -1,6 +1,11 @@ Go Time + Go Time + serial + Yes + TRUE + wubawuba All rights reserved https://changelog.com/gotime @@ -24,9 +29,16 @@ + + + + + awesome title full 1 + 1 + xxx 32:30 no