| @@ -0,0 +1,103 @@ | |||||
| import 'package:webfeed/domain/itunes/itunes_category.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_episode_type.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_image.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_owner.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_type.dart'; | |||||
| import 'package:webfeed/util/xml.dart'; | |||||
| import 'package:xml/xml.dart'; | |||||
| class Itunes { | |||||
| final String author; | |||||
| final String summary; | |||||
| final bool explicit; | |||||
| final String title; | |||||
| final String subtitle; | |||||
| final ItunesOwner owner; | |||||
| final List<String> keywords; | |||||
| final ItunesImage image; | |||||
| final List<ItunesCategory> categories; | |||||
| final ItunesType type; | |||||
| final String newFeedUrl; | |||||
| final bool block; | |||||
| final bool complete; | |||||
| final int episode; | |||||
| final int season; | |||||
| final Duration duration; | |||||
| final ItunesEpisodeType episodeType; | |||||
| Itunes({ | |||||
| this.author, | |||||
| this.summary, | |||||
| this.explicit, | |||||
| this.title, | |||||
| this.subtitle, | |||||
| this.owner, | |||||
| this.keywords, | |||||
| this.image, | |||||
| this.categories, | |||||
| this.type, | |||||
| this.newFeedUrl, | |||||
| this.block, | |||||
| this.complete, | |||||
| this.episode, | |||||
| this.season, | |||||
| this.duration, | |||||
| this.episodeType, | |||||
| }); | |||||
| factory Itunes.parse(XmlElement element) { | |||||
| if (element == null) { | |||||
| return null; | |||||
| } | |||||
| 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 Itunes( | |||||
| author: findElementOrNull(element, 'itunes:author')?.text?.trim(), | |||||
| summary: findElementOrNull(element, 'itunes:summary')?.text?.trim(), | |||||
| explicit: parseBoolLiteral(element, 'itunes:explicit'), | |||||
| title: findElementOrNull(element, 'itunes:title')?.text?.trim(), | |||||
| subtitle: findElementOrNull(element, 'itunes:subtitle')?.text?.trim(), | |||||
| owner: ItunesOwner.parse(findElementOrNull(element, 'itunes:owner')), | |||||
| keywords: findElementOrNull(element, 'itunes:keywords') | |||||
| ?.text | |||||
| ?.split(',') | |||||
| ?.map((keyword) => keyword.trim()) | |||||
| ?.toList(), | |||||
| image: ItunesImage.parse(findElementOrNull(element, 'itunes:image')), | |||||
| categories: findAllDirectElementsOrNull(element, 'itunes:category') | |||||
| .map((ele) => ItunesCategory.parse(ele)) | |||||
| .toList(), | |||||
| type: newItunesType(findElementOrNull(element, 'itunes:type')), | |||||
| newFeedUrl: | |||||
| findElementOrNull(element, 'itunes:new-feed-url')?.text?.trim(), | |||||
| block: parseBoolLiteral(element, 'itunes:block'), | |||||
| complete: parseBoolLiteral(element, 'itunes:complete'), | |||||
| episode: episodeStr == null ? null : int.parse(episodeStr), | |||||
| season: seasonStr == null ? null : int.parse(seasonStr), | |||||
| duration: durationStr == null ? null : _parseDuration(durationStr), | |||||
| episodeType: newItunesEpisodeType( | |||||
| findElementOrNull(element, 'itunes:episodeType')), | |||||
| ); | |||||
| } | |||||
| static 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, | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -1,12 +1,12 @@ | |||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| class RssItunesCategory { | |||||
| class ItunesCategory { | |||||
| final String category; | final String category; | ||||
| final List<String> subCategories; | final List<String> subCategories; | ||||
| RssItunesCategory({this.category, this.subCategories}); | |||||
| ItunesCategory({this.category, this.subCategories}); | |||||
| factory RssItunesCategory.parse(XmlElement element) { | |||||
| factory ItunesCategory.parse(XmlElement element) { | |||||
| if (element == null) return null; | if (element == null) return null; | ||||
| Iterable<XmlElement> subCategories; | Iterable<XmlElement> subCategories; | ||||
| @@ -15,7 +15,7 @@ class RssItunesCategory { | |||||
| } on StateError { | } on StateError { | ||||
| subCategories = null; | subCategories = null; | ||||
| } | } | ||||
| return RssItunesCategory( | |||||
| return ItunesCategory( | |||||
| category: element.getAttribute('text')?.trim(), | category: element.getAttribute('text')?.trim(), | ||||
| subCategories: subCategories | subCategories: subCategories | ||||
| ?.map((ele) => ele.getAttribute('text')?.trim()) | ?.map((ele) => ele.getAttribute('text')?.trim()) | ||||
| @@ -0,0 +1,16 @@ | |||||
| import 'package:xml/xml.dart'; | |||||
| enum ItunesEpisodeType { full, trailer, bonus, unknown } | |||||
| ItunesEpisodeType newItunesEpisodeType(XmlElement element) { | |||||
| switch (element?.text) { | |||||
| case 'full': | |||||
| return ItunesEpisodeType.full; | |||||
| case 'trailer': | |||||
| return ItunesEpisodeType.trailer; | |||||
| case 'bonus': | |||||
| return ItunesEpisodeType.bonus; | |||||
| default: | |||||
| return ItunesEpisodeType.unknown; | |||||
| } | |||||
| } | |||||
| @@ -1,13 +1,13 @@ | |||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| class RssItunesImage { | |||||
| class ItunesImage { | |||||
| final String href; | final String href; | ||||
| RssItunesImage({this.href}); | |||||
| ItunesImage({this.href}); | |||||
| factory RssItunesImage.parse(XmlElement element) { | |||||
| factory ItunesImage.parse(XmlElement element) { | |||||
| if (element == null) return null; | if (element == null) return null; | ||||
| return RssItunesImage( | |||||
| return ItunesImage( | |||||
| href: element.getAttribute('href')?.trim(), | href: element.getAttribute('href')?.trim(), | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,16 +1,15 @@ | |||||
| import 'package:webfeed/util/xml.dart'; | |||||
| import 'package:xml/xml.dart'; | import 'package:xml/xml.dart'; | ||||
| import '../util/xml.dart'; | |||||
| class RssItunesOwner { | |||||
| class ItunesOwner { | |||||
| final String name; | final String name; | ||||
| final String email; | final String email; | ||||
| RssItunesOwner({this.name, this.email}); | |||||
| ItunesOwner({this.name, this.email}); | |||||
| factory RssItunesOwner.parse(XmlElement element) { | |||||
| factory ItunesOwner.parse(XmlElement element) { | |||||
| if (element == null) return null; | if (element == null) return null; | ||||
| return RssItunesOwner( | |||||
| return ItunesOwner( | |||||
| name: findElementOrNull(element, 'itunes:name')?.text?.trim(), | name: findElementOrNull(element, 'itunes:name')?.text?.trim(), | ||||
| email: findElementOrNull(element, 'itunes:email')?.text?.trim(), | email: findElementOrNull(element, 'itunes:email')?.text?.trim(), | ||||
| ); | ); | ||||
| @@ -0,0 +1,14 @@ | |||||
| import 'package:xml/xml.dart'; | |||||
| enum ItunesType { episodic, serial, unknown } | |||||
| ItunesType newItunesType(XmlElement element) { | |||||
| switch (element?.text) { | |||||
| case 'episodic': | |||||
| return ItunesType.episodic; | |||||
| case 'serial': | |||||
| return ItunesType.serial; | |||||
| default: | |||||
| return ItunesType.unknown; | |||||
| } | |||||
| } | |||||
| @@ -1,6 +1,7 @@ | |||||
| import 'dart:core'; | import 'dart:core'; | ||||
| import 'package:webfeed/domain/dublin_core/dublin_core.dart'; | import 'package:webfeed/domain/dublin_core/dublin_core.dart'; | ||||
| import 'package:webfeed/domain/itunes/itunes.dart'; | |||||
| import 'package:webfeed/domain/rss_category.dart'; | import 'package:webfeed/domain/rss_category.dart'; | ||||
| import 'package:webfeed/domain/rss_cloud.dart'; | import 'package:webfeed/domain/rss_cloud.dart'; | ||||
| import 'package:webfeed/domain/rss_image.dart'; | import 'package:webfeed/domain/rss_image.dart'; | ||||
| @@ -8,8 +9,6 @@ import 'package:webfeed/domain/rss_item.dart'; | |||||
| import 'package:webfeed/util/xml.dart'; | import 'package:webfeed/util/xml.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; | ||||
| @@ -32,7 +31,7 @@ class RssFeed { | |||||
| final String webMaster; | final String webMaster; | ||||
| final int ttl; | final int ttl; | ||||
| final DublinCore dc; | final DublinCore dc; | ||||
| final RssItunes itunes; | |||||
| final Itunes itunes; | |||||
| RssFeed({ | RssFeed({ | ||||
| this.title, | this.title, | ||||
| @@ -102,7 +101,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), | |||||
| itunes: Itunes.parse(channelElement), | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,9 +1,9 @@ | |||||
| import 'package:webfeed/domain/dublin_core/dublin_core.dart'; | import 'package:webfeed/domain/dublin_core/dublin_core.dart'; | ||||
| import 'package:webfeed/domain/itunes/itunes.dart'; | |||||
| import 'package:webfeed/domain/media/media.dart'; | import 'package:webfeed/domain/media/media.dart'; | ||||
| import 'package:webfeed/domain/rss_category.dart'; | import 'package:webfeed/domain/rss_category.dart'; | ||||
| import 'package:webfeed/domain/rss_content.dart'; | import 'package:webfeed/domain/rss_content.dart'; | ||||
| import 'package:webfeed/domain/rss_enclosure.dart'; | import 'package:webfeed/domain/rss_enclosure.dart'; | ||||
| import 'package:webfeed/domain/rss_item_itunes.dart'; | |||||
| import 'package:webfeed/domain/rss_source.dart'; | import 'package:webfeed/domain/rss_source.dart'; | ||||
| import 'package:webfeed/util/datetime.dart'; | import 'package:webfeed/util/datetime.dart'; | ||||
| import 'package:webfeed/util/xml.dart'; | import 'package:webfeed/util/xml.dart'; | ||||
| @@ -24,7 +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; | |||||
| final Itunes itunes; | |||||
| RssItem({ | RssItem({ | ||||
| this.title, | this.title, | ||||
| @@ -60,7 +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), | |||||
| itunes: Itunes.parse(element), | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,89 +0,0 @@ | |||||
| import 'package:webfeed/util/xml.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 RssItunesEpisodeType episodeType; | |||||
| final String author; | |||||
| final String summary; | |||||
| final bool explicit; | |||||
| final String subtitle; | |||||
| final List<String> keywords; | |||||
| final RssItunesImage image; | |||||
| final RssItunesCategory category; | |||||
| final bool block; | |||||
| RssItemItunes({ | |||||
| this.title, | |||||
| this.episode, | |||||
| this.season, | |||||
| this.duration, | |||||
| this.episodeType, | |||||
| this.author, | |||||
| this.summary, | |||||
| this.explicit, | |||||
| this.subtitle, | |||||
| this.keywords, | |||||
| this.image, | |||||
| this.category, | |||||
| this.block, | |||||
| }); | |||||
| factory RssItemItunes.parse(XmlElement element) { | |||||
| if (element == null) { | |||||
| return null; | |||||
| } | |||||
| 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: newRssItunesEpisodeType( | |||||
| findElementOrNull(element, 'itunes:episodeType')), | |||||
| author: findElementOrNull(element, 'itunes:author')?.text?.trim(), | |||||
| summary: findElementOrNull(element, 'itunes:summary')?.text?.trim(), | |||||
| 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'), | |||||
| ); | |||||
| } | |||||
| } | |||||
| 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, | |||||
| ); | |||||
| } | |||||
| @@ -1,67 +0,0 @@ | |||||
| import 'package:webfeed/util/xml.dart'; | |||||
| import 'package:xml/xml.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<String> keywords; | |||||
| final RssItunesImage image; | |||||
| final List<RssItunesCategory> 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.categories, | |||||
| this.type, | |||||
| this.newFeedUrl, | |||||
| this.block, | |||||
| this.complete, | |||||
| }); | |||||
| factory RssItunes.parse(XmlElement element) { | |||||
| if (element == null) { | |||||
| return null; | |||||
| } | |||||
| return RssItunes( | |||||
| author: findElementOrNull(element, 'itunes:author')?.text?.trim(), | |||||
| summary: findElementOrNull(element, 'itunes:summary')?.text?.trim(), | |||||
| 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(), | |||||
| image: RssItunesImage.parse(findElementOrNull(element, 'itunes:image')), | |||||
| 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'), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @@ -1,19 +0,0 @@ | |||||
| 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; | |||||
| } | |||||
| } | |||||
| @@ -1,17 +0,0 @@ | |||||
| 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; | |||||
| } | |||||
| } | |||||
| @@ -22,6 +22,6 @@ List<XmlElement> findAllDirectElementsOrNull(XmlElement element, String name, | |||||
| bool parseBoolLiteral(XmlElement element, String tagName) { | bool parseBoolLiteral(XmlElement element, String tagName) { | ||||
| var v = findElementOrNull(element, tagName)?.text?.toLowerCase()?.trim(); | var v = findElementOrNull(element, tagName)?.text?.toLowerCase()?.trim(); | ||||
| if (v == null) return null; | |||||
| if (v == null) return false; | |||||
| return ['yes', 'true'].contains(v); | return ['yes', 'true'].contains(v); | ||||
| } | } | ||||
| @@ -2,8 +2,8 @@ import 'dart:core'; | |||||
| import 'dart:io'; | import 'dart:io'; | ||||
| import 'package:test/test.dart'; | import 'package:test/test.dart'; | ||||
| import 'package:webfeed/domain/rss_itunes_episode_type.dart'; | |||||
| import 'package:webfeed/domain/rss_itunes_type.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_episode_type.dart'; | |||||
| import 'package:webfeed/domain/itunes/itunes_type.dart'; | |||||
| import 'package:webfeed/webfeed.dart'; | import 'package:webfeed/webfeed.dart'; | ||||
| void main() { | void main() { | ||||
| @@ -344,13 +344,13 @@ void main() { | |||||
| } | } | ||||
| } | } | ||||
| expect(feed.itunes.title, 'Go Time'); | expect(feed.itunes.title, 'Go Time'); | ||||
| expect(feed.itunes.type, RssItunesType.serial); | |||||
| expect(feed.itunes.type, ItunesType.serial); | |||||
| expect(feed.itunes.newFeedUrl, 'wubawuba'); | expect(feed.itunes.newFeedUrl, 'wubawuba'); | ||||
| expect(feed.itunes.block, true); | expect(feed.itunes.block, true); | ||||
| expect(feed.itunes.complete, true); | expect(feed.itunes.complete, true); | ||||
| var item = feed.items[0]; | var item = feed.items[0]; | ||||
| expect(item.itunes.episodeType, RssItunesEpisodeType.full); | |||||
| expect(item.itunes.episodeType, ItunesEpisodeType.full); | |||||
| expect(item.itunes.episode, 1); | expect(item.itunes.episode, 1); | ||||
| expect(item.itunes.season, 1); | expect(item.itunes.season, 1); | ||||
| expect(item.itunes.image.href, | expect(item.itunes.image.href, | ||||