diff --git a/lib/domain/podcast/podcast.dart b/lib/domain/podcast/podcast.dart new file mode 100644 index 0000000..0fde8af --- /dev/null +++ b/lib/domain/podcast/podcast.dart @@ -0,0 +1,90 @@ +import 'package:webfeed/domain/media/license.dart'; +import 'package:webfeed/domain/podcast/podcast_funding.dart'; +import 'package:webfeed/domain/podcast/podcast_guid.dart'; +import 'package:webfeed/domain/podcast/podcast_images.dart'; +import 'package:webfeed/domain/podcast/podcast_live_item.dart'; +import 'package:webfeed/domain/podcast/podcast_location.dart'; +import 'package:webfeed/domain/podcast/podcast_locked.dart'; +import 'package:webfeed/domain/podcast/podcast_medium.dart'; +import 'package:webfeed/domain/podcast/podcast_person.dart'; +import 'package:webfeed/domain/podcast/podcast_trailer.dart'; +import 'package:webfeed/domain/podcast/podcast_value.dart'; +import 'package:webfeed/util/iterable.dart'; +import 'package:xml/xml.dart'; + +class Podcast { + final Locked? locked; + final Funding? funding; + final List people; + final Location? location; + final List trailers; + final License? license; + final Guid? guid; + final Value? value; + final Medium? medium; + final Images? images; + final List liveItems; + + Podcast({ + this.locked, + this.funding, + required this.people, + this.location, + required this.trailers, + this.license, + this.guid, + this.value, + this.medium, + this.images, + required this.liveItems, + }); + + factory Podcast.parse(XmlElement element) { + return Podcast( + locked: element + .findElements('podcast:locked') + .map((e) => Locked.parse(e)) + .firstOrNull, + funding: element + .findElements('podcast:funding') + .map((e) => Funding.parse(e)) + .firstOrNull, + people: element + .findElements('podcast:person') + .map((e) => Person.parse(e)) + .toList(), + location: element + .findElements('podcast:location') + .map((e) => Location.parse(e)) + .firstOrNull, + trailers: element + .findElements('podcast:trailer') + .map((e) => Trailer.parse(e)) + .toList(), + license: element + .findElements('podcast:license') + .map((e) => License.parse(e)) + .firstOrNull, + guid: element + .findElements('podcast:guid') + .map((e) => Guid.parse(e)) + .firstOrNull, + value: element + .findElements('podcast:value') + .map((e) => Value.parse(e)) + .firstOrNull, + medium: element + .findElements('podcast:medium') + .map((e) => Medium.parse(e)) + .firstOrNull, + images: element + .findElements('podcast:images') + .map((e) => Images.parse(e)) + .firstOrNull, + liveItems: element + .findElements('podcast:liveItem') + .map((e) => LiveItem.parse(e)) + .toList(), + ); + } +} diff --git a/lib/domain/podcast/podcast_alternate_enclosure.dart b/lib/domain/podcast/podcast_alternate_enclosure.dart new file mode 100644 index 0000000..38ff947 --- /dev/null +++ b/lib/domain/podcast/podcast_alternate_enclosure.dart @@ -0,0 +1,57 @@ +import 'package:webfeed/domain/podcast/podcast_integrity.dart'; +import 'package:webfeed/domain/podcast/podcast_source.dart'; +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#alternate-enclosure +class AlternateEnclosure { + final String? type; + final int? length; + final int? bitrate; + final int? height; + final String? lang; + final String? title; + final String? rel; + final String? codecs; + final String? default_; + final List sources; + final List integrities; + + AlternateEnclosure({ + this.type, + this.length, + this.bitrate, + this.height, + this.lang, + this.title, + this.rel, + this.codecs, + this.default_, + required this.sources, + required this.integrities, + }); + + factory AlternateEnclosure.parse(XmlElement element) { + final lengthStr = element.getAttribute('length'); + final bitrateStr = element.getAttribute('bitrate'); + final heightStr = element.getAttribute('height'); + return AlternateEnclosure( + type: element.getAttribute('type'), + length: lengthStr == null ? null : int.tryParse(lengthStr), + bitrate: bitrateStr == null ? null : int.tryParse(bitrateStr), + height: heightStr == null ? null : int.tryParse(heightStr), + lang: element.getAttribute('lang'), + title: element.getAttribute('title'), + rel: element.getAttribute('rel'), + codecs: element.getAttribute('codecs'), + default_: element.getAttribute('default'), + sources: element + .findElements('podcast:source') + .map((e) => Source.parse(e)) + .toList(), + integrities: element + .findElements('podcast:integrity') + .map((e) => Integrity.parse(e)) + .toList(), + ); + } +} diff --git a/lib/domain/podcast/podcast_chapters.dart b/lib/domain/podcast/podcast_chapters.dart new file mode 100644 index 0000000..ec2c546 --- /dev/null +++ b/lib/domain/podcast/podcast_chapters.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#chapters +class Chapters { + final String? url; + final String? type; + + Chapters({ + this.url, + this.type, + }); + + factory Chapters.parse(XmlElement element) { + return Chapters( + url: element.getAttribute('url'), + type: element.getAttribute('type'), + ); + } +} diff --git a/lib/domain/podcast/podcast_content_link.dart b/lib/domain/podcast/podcast_content_link.dart new file mode 100644 index 0000000..4f2de79 --- /dev/null +++ b/lib/domain/podcast/podcast_content_link.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#content-link +class ContentLink { + final String? href; + final String? value; + + ContentLink({ + this.href, + this.value, + }); + + factory ContentLink.parse(XmlElement element) { + return ContentLink( + href: element.getAttribute('href'), + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_episode.dart b/lib/domain/podcast/podcast_episode.dart new file mode 100644 index 0000000..8eb86c8 --- /dev/null +++ b/lib/domain/podcast/podcast_episode.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#episode +class Episode { + final String? display; + final double? value; + + Episode({ + this.display, + this.value, + }); + + factory Episode.parse(XmlElement element) { + return Episode( + display: element.getAttribute('display'), + value: double.tryParse(element.text), + ); + } +} diff --git a/lib/domain/podcast/podcast_funding.dart b/lib/domain/podcast/podcast_funding.dart new file mode 100644 index 0000000..b13d185 --- /dev/null +++ b/lib/domain/podcast/podcast_funding.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#funding +class Funding { + final String? url; + final String? value; + + Funding({ + this.url, + this.value, + }); + + factory Funding.parse(XmlElement element) { + return Funding( + url: element.getAttribute('url'), + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_guid.dart b/lib/domain/podcast/podcast_guid.dart new file mode 100644 index 0000000..4f59e6f --- /dev/null +++ b/lib/domain/podcast/podcast_guid.dart @@ -0,0 +1,16 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#guid +class Guid { + final String? value; + + Guid({ + this.value, + }); + + factory Guid.parse(XmlElement element) { + return Guid( + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_images.dart b/lib/domain/podcast/podcast_images.dart new file mode 100644 index 0000000..9415786 --- /dev/null +++ b/lib/domain/podcast/podcast_images.dart @@ -0,0 +1,31 @@ +import 'package:xml/xml.dart'; + +final exp = RegExp(r'([^ ]+) (\d)+w'); + +class Src { + final String? url; + final int? width; + + Src({required this.url, required this.width}); +} + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#images +class Images { + final List srcset; + + Images({ + required this.srcset, + }); + + factory Images.parse(XmlElement element) { + return Images( + srcset: element.getAttribute('srcset')?.split(', ').map((e) { + final matches = exp.allMatches(e).toList(); + return Src( + url: matches[0].toString(), + width: int.tryParse(matches[1].toString())); + }).toList() ?? + [], + ); + } +} diff --git a/lib/domain/podcast/podcast_integrity.dart b/lib/domain/podcast/podcast_integrity.dart new file mode 100644 index 0000000..2aef4c9 --- /dev/null +++ b/lib/domain/podcast/podcast_integrity.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#integrity +class Integrity { + final String? type; + final String? value; + + Integrity({ + this.type, + this.value, + }); + + factory Integrity.parse(XmlElement element) { + return Integrity( + type: element.getAttribute('type'), + value: element.getAttribute('value'), + ); + } +} diff --git a/lib/domain/podcast/podcast_item.dart b/lib/domain/podcast/podcast_item.dart new file mode 100644 index 0000000..2fe2483 --- /dev/null +++ b/lib/domain/podcast/podcast_item.dart @@ -0,0 +1,97 @@ +import 'package:webfeed/domain/media/license.dart'; +import 'package:webfeed/domain/podcast/podcast_alternate_enclosure.dart'; +import 'package:webfeed/domain/podcast/podcast_chapters.dart'; +import 'package:webfeed/domain/podcast/podcast_content_link.dart'; +import 'package:webfeed/domain/podcast/podcast_episode.dart'; +import 'package:webfeed/domain/podcast/podcast_images.dart'; +import 'package:webfeed/domain/podcast/podcast_location.dart'; +import 'package:webfeed/domain/podcast/podcast_person.dart'; +import 'package:webfeed/domain/podcast/podcast_season.dart'; +import 'package:webfeed/domain/podcast/podcast_soundbite.dart'; +import 'package:webfeed/domain/podcast/podcast_transcript.dart'; +import 'package:webfeed/domain/podcast/podcast_value.dart'; +import 'package:webfeed/util/iterable.dart'; +import 'package:xml/xml.dart'; + +class PodcastItem { + final Transcript? transcript; + final Chapters? chapters; + final List soundbites; + final List people; + final Location? location; + final Season? season; + final Episode? episode; + final License? license; + final List alternateEnclosures; + final Value? value; + final Images? images; + final List contentLinks; + + PodcastItem({ + this.transcript, + this.chapters, + required this.soundbites, + required this.people, + this.location, + this.season, + this.episode, + this.license, + required this.alternateEnclosures, + this.value, + this.images, + required this.contentLinks, + }); + + factory PodcastItem.parse(XmlElement element) { + return PodcastItem( + transcript: element + .findElements('podcast:transcript') + .map((e) => Transcript.parse(e)) + .firstOrNull, + chapters: element + .findElements('podcast:chapters') + .map((e) => Chapters.parse(e)) + .firstOrNull, + soundbites: element + .findElements('podcast:soundbite') + .map((e) => Soundbite.parse(e)) + .toList(), + people: element + .findElements('podcast:person') + .map((e) => Person.parse(e)) + .toList(), + location: element + .findElements('podcast:location') + .map((e) => Location.parse(e)) + .firstOrNull, + season: element + .findElements('podcast:season') + .map((e) => Season.parse(e)) + .firstOrNull, + episode: element + .findElements('podcast:episode') + .map((e) => Episode.parse(e)) + .firstOrNull, + license: element + .findElements('podcast:license') + .map((e) => License.parse(e)) + .firstOrNull, + alternateEnclosures: element + .findElements('podcast:alternateEnclosure') + .map((e) => AlternateEnclosure.parse(e)) + .toList(), + value: element + .findElements('podcast:value') + .map((e) => Value.parse(e)) + .firstOrNull, + images: element + .findElements('podcast:images') + .map((e) => Images.parse(e)) + .firstOrNull, + contentLinks: element + .findElements('podcast:contentLink') + .map((e) => ContentLink.parse(e)) + .toList(), + ); + } +} diff --git a/lib/domain/podcast/podcast_live_item.dart b/lib/domain/podcast/podcast_live_item.dart new file mode 100644 index 0000000..c5ae4d7 --- /dev/null +++ b/lib/domain/podcast/podcast_live_item.dart @@ -0,0 +1,43 @@ +import 'package:webfeed/domain/rss_item.dart'; +import 'package:xml/xml.dart'; + +enum LiveItemStatus { pending, live, ended } + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#live-item +class LiveItem { + final LiveItemStatus? status; + final DateTime? start; + final DateTime? end; + final RssItem item; + + LiveItem({ + this.status, + this.start, + this.end, + required this.item, + }); + + factory LiveItem.parse(XmlElement element) { + final startStr = element.getAttribute('start'); + final endStr = element.getAttribute('end'); + return LiveItem( + status: newPodcastLiveItemStatusType(element.getAttribute('status')), + start: startStr == null ? null : DateTime.tryParse(startStr), + end: endStr == null ? null : DateTime.tryParse(endStr), + item: RssItem.parse(element), + ); + } +} + +LiveItemStatus? newPodcastLiveItemStatusType(String? value) { + switch (value) { + case 'pending': + return LiveItemStatus.pending; + case 'live': + return LiveItemStatus.live; + case 'ended': + return LiveItemStatus.ended; + default: + return null; + } +} diff --git a/lib/domain/podcast/podcast_location.dart b/lib/domain/podcast/podcast_location.dart new file mode 100644 index 0000000..b4df22a --- /dev/null +++ b/lib/domain/podcast/podcast_location.dart @@ -0,0 +1,22 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#location +class Location { + final String? geo; + final String? osm; + final String? value; + + Location({ + this.geo, + this.osm, + this.value, + }); + + factory Location.parse(XmlElement element) { + return Location( + geo: element.getAttribute('geo'), + osm: element.getAttribute('osm'), + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_locked.dart b/lib/domain/podcast/podcast_locked.dart new file mode 100644 index 0000000..19e18fc --- /dev/null +++ b/lib/domain/podcast/podcast_locked.dart @@ -0,0 +1,31 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#locked +class Locked { + final String? owner; + final bool? value; + + Locked({ + this.owner, + this.value, + }); + + factory Locked.parse(XmlElement element) { + bool? valueBool; + switch (element.text.toLowerCase()) { + case 'yes': + valueBool = true; + break; + case 'no': + valueBool = false; + break; + default: + valueBool = null; + break; + } + return Locked( + owner: element.getAttribute('owner'), + value: valueBool, + ); + } +} diff --git a/lib/domain/podcast/podcast_medium.dart b/lib/domain/podcast/podcast_medium.dart new file mode 100644 index 0000000..3f4af6c --- /dev/null +++ b/lib/domain/podcast/podcast_medium.dart @@ -0,0 +1,39 @@ +import 'package:xml/xml.dart'; + +enum MediumType { podcast, music, video, film, audiobook, newsletter, blog } + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#medium +class Medium { + final MediumType value; + + Medium({ + required this.value, + }); + + factory Medium.parse(XmlElement element) { + return Medium( + value: newPodcastMediumType(element.text), + ); + } +} + +MediumType newPodcastMediumType(String value) { + switch (value) { + case 'podcast': + return MediumType.podcast; + case 'music': + return MediumType.music; + case 'video': + return MediumType.video; + case 'film': + return MediumType.film; + case 'audiobook': + return MediumType.audiobook; + case 'newletter': + return MediumType.newsletter; + case 'blog': + return MediumType.blog; + default: + return MediumType.podcast; + } +} diff --git a/lib/domain/podcast/podcast_person.dart b/lib/domain/podcast/podcast_person.dart new file mode 100644 index 0000000..6ab5626 --- /dev/null +++ b/lib/domain/podcast/podcast_person.dart @@ -0,0 +1,28 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#person +class Person { + final String? role; + final String? group; + final String? img; + final String? href; + final String? value; + + Person({ + this.role, + this.group, + this.img, + this.href, + this.value, + }); + + factory Person.parse(XmlElement element) { + return Person( + role: element.getAttribute('role'), + group: element.getAttribute('group'), + img: element.getAttribute('img'), + href: element.getAttribute('href'), + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_season.dart b/lib/domain/podcast/podcast_season.dart new file mode 100644 index 0000000..40d0e4b --- /dev/null +++ b/lib/domain/podcast/podcast_season.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#season +class Season { + final String? name; + final int? value; + + Season({ + this.name, + this.value, + }); + + factory Season.parse(XmlElement element) { + return Season( + name: element.getAttribute('name'), + value: int.tryParse(element.text), + ); + } +} diff --git a/lib/domain/podcast/podcast_soundbite.dart b/lib/domain/podcast/podcast_soundbite.dart new file mode 100644 index 0000000..6ca350d --- /dev/null +++ b/lib/domain/podcast/podcast_soundbite.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#soundbite +class Soundbite { + final double? startTime; + final double? duration; + final String? value; + + Soundbite({ + this.startTime, + this.duration, + this.value, + }); + + factory Soundbite.parse(XmlElement element) { + final startTimeStr = element.getAttribute('startTime'); + final durationStr = element.getAttribute('duration'); + return Soundbite( + startTime: startTimeStr != null ? double.tryParse(startTimeStr) : null, + duration: durationStr != null ? double.tryParse(durationStr) : null, + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_source.dart b/lib/domain/podcast/podcast_source.dart new file mode 100644 index 0000000..3661f30 --- /dev/null +++ b/lib/domain/podcast/podcast_source.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#source +class Source { + final String? url; + final String? contentType; + + Source({ + this.url, + this.contentType, + }); + + factory Source.parse(XmlElement element) { + return Source( + url: element.getAttribute('url'), + contentType: element.getAttribute('contentType'), + ); + } +} diff --git a/lib/domain/podcast/podcast_trailer.dart b/lib/domain/podcast/podcast_trailer.dart new file mode 100644 index 0000000..43552c4 --- /dev/null +++ b/lib/domain/podcast/podcast_trailer.dart @@ -0,0 +1,33 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#trailer +class Trailer { + final String? url; + final String? pubdate; + final int? length; + final String? type; + final int? season; + final String? value; + + Trailer({ + this.url, + this.pubdate, + this.length, + this.type, + this.season, + this.value, + }); + + factory Trailer.parse(XmlElement element) { + final lengthStr = element.getAttribute('length'); + final seasonStr = element.getAttribute('season'); + return Trailer( + url: element.getAttribute('url'), + pubdate: element.getAttribute('pubdate'), + length: lengthStr == null ? null : int.tryParse(lengthStr), + type: element.getAttribute('type'), + season: seasonStr == null ? null : int.tryParse(seasonStr), + value: element.text, + ); + } +} diff --git a/lib/domain/podcast/podcast_transcript.dart b/lib/domain/podcast/podcast_transcript.dart new file mode 100644 index 0000000..28e909b --- /dev/null +++ b/lib/domain/podcast/podcast_transcript.dart @@ -0,0 +1,25 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#transcript +class Transcript { + final String? url; + final String? type; + final String? language; + final String? rel; + + Transcript({ + this.url, + this.type, + this.language, + this.rel, + }); + + factory Transcript.parse(XmlElement element) { + return Transcript( + url: element.getAttribute('url'), + type: element.getAttribute('type'), + language: element.getAttribute('language'), + rel: element.getAttribute('rel'), + ); + } +} diff --git a/lib/domain/podcast/podcast_value.dart b/lib/domain/podcast/podcast_value.dart new file mode 100644 index 0000000..6bc8093 --- /dev/null +++ b/lib/domain/podcast/podcast_value.dart @@ -0,0 +1,29 @@ +import 'package:webfeed/domain/podcast/podcast_value_recipient.dart'; +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#value +class Value { + final String? type; + final String? method; + final String? suggested; + final List valueRecipients; + + Value({ + this.type, + this.method, + this.suggested, + required this.valueRecipients, + }); + + factory Value.parse(XmlElement element) { + return Value( + type: element.getAttribute('type'), + method: element.getAttribute('method'), + suggested: element.getAttribute('suggested'), + valueRecipients: element + .findElements('podcast:valueRecipient') + .map((e) => ValueRecipient.parse(e)) + .toList(), + ); + } +} diff --git a/lib/domain/podcast/podcast_value_recipient.dart b/lib/domain/podcast/podcast_value_recipient.dart new file mode 100644 index 0000000..4f0d3e5 --- /dev/null +++ b/lib/domain/podcast/podcast_value_recipient.dart @@ -0,0 +1,36 @@ +import 'package:xml/xml.dart'; + +// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#value-recipient +class ValueRecipient { + final String? name; + final String? customKey; + final String? customValue; + final String? type; + final String? address; + final int? split; + final bool fee; + + ValueRecipient({ + this.name, + this.customKey, + this.customValue, + this.type, + this.address, + this.split, + required this.fee, + }); + + factory ValueRecipient.parse(XmlElement element) { + final splitStr = element.getAttribute('split'); + final feeStr = element.getAttribute('fee'); + return ValueRecipient( + name: element.getAttribute('name'), + customKey: element.getAttribute('customKey'), + customValue: element.getAttribute('customValue'), + type: element.getAttribute('type'), + address: element.getAttribute('address'), + split: splitStr == null ? null : int.tryParse(splitStr), + fee: feeStr?.toLowerCase() == 'true', + ); + } +} diff --git a/lib/domain/rss_feed.dart b/lib/domain/rss_feed.dart index 4408f37..1869baf 100644 --- a/lib/domain/rss_feed.dart +++ b/lib/domain/rss_feed.dart @@ -2,6 +2,7 @@ import 'dart:core'; import 'package:webfeed/domain/dublin_core/dublin_core.dart'; import 'package:webfeed/domain/itunes/itunes.dart'; +import 'package:webfeed/domain/podcast/podcast.dart'; import 'package:webfeed/domain/rss_category.dart'; import 'package:webfeed/domain/rss_cloud.dart'; import 'package:webfeed/domain/rss_image.dart'; @@ -34,6 +35,7 @@ class RssFeed { final DublinCore? dc; final Itunes? itunes; final Syndication? syndication; + final Podcast podcast; RssFeed({ this.title, @@ -58,6 +60,7 @@ class RssFeed { this.dc, this.itunes, this.syndication, + required this.podcast, }); factory RssFeed.parse(String xmlString) { @@ -121,6 +124,7 @@ class RssFeed { dc: DublinCore.parse(channelElement), itunes: Itunes.parse(channelElement), syndication: Syndication.parse(channelElement), + podcast: Podcast.parse(channelElement), ); } } diff --git a/lib/domain/rss_item.dart b/lib/domain/rss_item.dart index ef9ff20..e3fa138 100644 --- a/lib/domain/rss_item.dart +++ b/lib/domain/rss_item.dart @@ -1,6 +1,7 @@ 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/podcast/podcast_item.dart'; import 'package:webfeed/domain/rss_category.dart'; import 'package:webfeed/domain/rss_content.dart'; import 'package:webfeed/domain/rss_enclosure.dart'; @@ -25,6 +26,7 @@ class RssItem { final RssEnclosure? enclosure; final DublinCore? dc; final Itunes? itunes; + final PodcastItem podcast; RssItem({ this.title, @@ -41,6 +43,7 @@ class RssItem { this.enclosure, this.dc, this.itunes, + required this.podcast, }); factory RssItem.parse(XmlElement element) { @@ -71,6 +74,7 @@ class RssItem { .firstOrNull, dc: DublinCore.parse(element), itunes: Itunes.parse(element), + podcast: PodcastItem.parse(element), ); } } diff --git a/test/rss_test.dart b/test/rss_test.dart index fa3d789..45f3d3c 100644 --- a/test/rss_test.dart +++ b/test/rss_test.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:test/test.dart'; import 'package:webfeed/domain/itunes/itunes_episode_type.dart'; import 'package:webfeed/domain/itunes/itunes_type.dart'; +import 'package:webfeed/domain/podcast/podcast_live_item.dart'; +import 'package:webfeed/domain/podcast/podcast_medium.dart'; import 'package:webfeed/domain/syndication/syndication.dart'; import 'package:webfeed/webfeed.dart'; @@ -421,4 +423,137 @@ void main() { 'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.'); expect(feed.items!.first.link, 'http://c.moreover.com/click/here.pl?r123'); }); + + test('parse RSS-Podcast.xml', () { + var xmlString = File('test/xml/RSS-Podcast.xml').readAsStringSync(); + + var feed = RssFeed.parse(xmlString); + + expect(feed.podcast.locked!.owner, 'email@example.com'); + expect(feed.podcast.locked!.value, true); + expect(feed.podcast.funding!.url, 'https://www.example.com/donations'); + expect(feed.podcast.funding!.value, 'Support the show!'); + expect(feed.podcast.people.length, 1); + expect( + feed.podcast.people.first.href, 'https://example.com/johnsmith/blog'); + expect(feed.podcast.people.first.img, + 'http://example.com/images/johnsmith.jpg'); + expect(feed.podcast.people.first.value, 'John Smith'); + expect(feed.podcast.location!.geo, 'geo:30.2672,97.7431'); + expect(feed.podcast.location!.osm, 'R113314'); + expect(feed.podcast.location!.value, 'Austin, TX'); + expect(feed.podcast.trailers.length, 1); + expect( + feed.podcast.trailers.first.pubdate, 'Thu, 01 Apr 2021 08:00:00 EST'); + expect( + feed.podcast.trailers.first.url, 'https://example.org/trailers/teaser'); + expect(feed.podcast.trailers.first.length, 12345678); + expect(feed.podcast.trailers.first.type, 'audio/mp3'); + expect(feed.podcast.trailers.first.value, 'Coming April 1st, 2021'); + expect(feed.podcast.license!.value, 'cc-by-4.0'); + expect(feed.podcast.license!.url, null); + expect(feed.podcast.guid!.value, '917393e3-1b1e-5cef-ace4-edaa54e1f810'); + expect(feed.podcast.value!.type, 'lightning'); + expect(feed.podcast.value!.method, 'keysend'); + expect(feed.podcast.value!.suggested, '0.00000015000'); + expect(feed.podcast.value!.valueRecipients.length, 2); + expect(feed.podcast.value!.valueRecipients.first.name, 'Alice (Podcaster)'); + expect(feed.podcast.value!.valueRecipients.first.type, 'node'); + expect(feed.podcast.value!.valueRecipients.first.address, + '02d5c1bf8b940dc9cadca86d1b0a3c37fbe39cee4c7e839e33bef9174531d27f52'); + expect(feed.podcast.value!.valueRecipients.first.split, 100); + expect(feed.podcast.value!.valueRecipients.first.fee, false); + expect(feed.podcast.value!.valueRecipients.last.name, 'Hosting Provider'); + expect(feed.podcast.value!.valueRecipients.last.type, 'node'); + expect(feed.podcast.value!.valueRecipients.last.address, + '03ae9f91a0cb8ff43840e3c322c4c61f019d8c1c3cea15a25cfc425ac605e61a4a'); + expect(feed.podcast.value!.valueRecipients.last.split, 5); + expect(feed.podcast.value!.valueRecipients.last.fee, true); + expect(feed.podcast.medium!.value, MediumType.podcast); + expect(feed.podcast.images!.srcset.length, 4); + expect(feed.podcast.images!.srcset[0].url, + 'https://example.com/images/ep1/pci_avatar-massive.jpg'); + expect(feed.podcast.images!.srcset[0].width, 1500); + expect(feed.podcast.images!.srcset[1].url, + 'https://example.com/images/ep1/pci_avatar-middle.jpg'); + expect(feed.podcast.images!.srcset[1].width, 600); + expect(feed.podcast.images!.srcset[2].url, + 'https://example.com/images/ep1/pci_avatar-small.jpg'); + expect(feed.podcast.images!.srcset[2].width, 300); + expect(feed.podcast.images!.srcset[3].url, + 'https://example.com/images/ep1/pci_avatar-tiny.jpg'); + expect(feed.podcast.images!.srcset[3].width, 150); + expect(feed.items!.length, 1); + expect(feed.items!.first.podcast.transcript!.url, + 'https://example.com/episode1/transcript.json'); + expect(feed.items!.first.podcast.transcript!.type, 'application/json'); + expect(feed.items!.first.podcast.transcript!.language, 'es'); + expect(feed.items!.first.podcast.transcript!.rel, 'captions'); + expect(feed.items!.first.podcast.chapters!.url, + 'https://example.com/episode1/chapters.json'); + expect( + feed.items!.first.podcast.chapters!.type, 'application/json+chapters'); + expect(feed.items!.first.podcast.soundbites.length, 1); + expect(feed.items!.first.podcast.soundbites.first.startTime, 1234.5); + expect(feed.items!.first.podcast.soundbites.first.duration, 42.25); + expect(feed.items!.first.podcast.soundbites.first.value, + 'Why the Podcast Namespace Matters'); + expect(feed.items!.first.podcast.license!.url, + 'https://example.org/mypodcastlicense/full.pdf'); + expect( + feed.items!.first.podcast.season!.name, 'Race for the Whitehouse 2020'); + expect(feed.items!.first.podcast.season!.value, 3); + expect(feed.items!.first.podcast.episode!.display, 'Ch.3'); + expect(feed.items!.first.podcast.episode!.value, 204); + expect(feed.items!.first.podcast.location!.geo, 'geo:33.51601,-86.81455'); + expect(feed.items!.first.podcast.location!.osm, 'R6930627'); + expect(feed.items!.first.podcast.location!.value, + 'Birmingham Civil Rights Museum'); + expect(feed.items!.first.podcast.alternateEnclosures.length, 1); + expect( + feed.items!.first.podcast.alternateEnclosures.first.type, 'video/mp4'); + expect(feed.items!.first.podcast.alternateEnclosures.first.length, 7924786); + expect( + feed.items!.first.podcast.alternateEnclosures.first.bitrate, 511276.52); + expect(feed.items!.first.podcast.alternateEnclosures.first.height, 720); + expect( + feed.items!.first.podcast.alternateEnclosures.first.sources.length, 1); + expect( + feed.items!.first.podcast.alternateEnclosures.first.sources.first.url, + 'https://example.com/file-720.mp4'); + expect( + feed.items!.first.podcast.alternateEnclosures.first.sources.first + .contentType, + null); + expect( + feed.items!.first.podcast.alternateEnclosures.first.integrities.first + .type, + 'sri'); + expect( + feed.items!.first.podcast.alternateEnclosures.first.integrities.first + .value, + 'sha384-ExVqijgYHm15PqQqdXfW95x+Rs6C+d6E/ICxyQOeFevnxNLR/wtJNrNYTjIysUBo'); + expect( + feed.items!.first.podcast.alternateEnclosures.first.integrities.length, + 1); + expect(feed.podcast.liveItems.length, 1); + expect(feed.podcast.liveItems.first.status, LiveItemStatus.live); + expect(feed.podcast.liveItems.first.start, + DateTime.utc(2021, 9, 26, 7, 30, 0)); + expect( + feed.podcast.liveItems.first.end, DateTime.utc(2021, 9, 26, 9, 30, 0)); + expect( + feed.podcast.liveItems.first.item.title, 'Podcasting 2.0 Live Stream'); + expect(feed.podcast.liveItems.first.item.guid, + 'e32b4890-983b-4ce5-8b46-f2d6bc1d8819'); + expect(feed.podcast.liveItems.first.item.enclosure!.url, + 'https://example.com/pc20/livestream?format=.mp3'); + expect(feed.podcast.liveItems.first.item.enclosure!.type, 'audio/mpeg'); + expect(feed.podcast.liveItems.first.item.enclosure!.length, 312); + expect(feed.podcast.liveItems.first.item.podcast.contentLinks.length, 1); + expect(feed.podcast.liveItems.first.item.podcast.contentLinks.first.href, + 'https://example.com/html/livestream'); + expect(feed.podcast.liveItems.first.item.podcast.contentLinks.first.value, + 'Listen Live!'); + }); } diff --git a/test/xml/RSS-Podcast.xml b/test/xml/RSS-Podcast.xml new file mode 100644 index 0000000..c244a3a --- /dev/null +++ b/test/xml/RSS-Podcast.xml @@ -0,0 +1,52 @@ + + + yes + Support the show! + John Smith + Austin, TX + Coming April 1st, 2021 + cc-by-4.0 + 917393e3-1b1e-5cef-ace4-edaa54e1f810 + + + + + podcast + + + + + Why the Podcast Namespace Matters + my-podcast-license-v1 + 3 + 204 + Birmingham Civil Rights Museum + + + + + + + Podcasting 2.0 Live Stream + e32b4890-983b-4ce5-8b46-f2d6bc1d8819 + + Listen Live! + + + \ No newline at end of file