├── .github └── workflows │ └── dart.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── domain │ ├── atom_category.dart │ ├── atom_feed.dart │ ├── atom_generator.dart │ ├── atom_item.dart │ ├── atom_link.dart │ ├── atom_person.dart │ ├── atom_source.dart │ ├── dublin_core │ │ └── dublin_core.dart │ ├── itunes │ │ ├── itunes.dart │ │ ├── itunes_category.dart │ │ ├── itunes_episode_type.dart │ │ ├── itunes_image.dart │ │ ├── itunes_owner.dart │ │ └── itunes_type.dart │ ├── media │ │ ├── category.dart │ │ ├── community.dart │ │ ├── content.dart │ │ ├── copyright.dart │ │ ├── credit.dart │ │ ├── description.dart │ │ ├── embed.dart │ │ ├── group.dart │ │ ├── hash.dart │ │ ├── license.dart │ │ ├── media.dart │ │ ├── param.dart │ │ ├── peer_link.dart │ │ ├── player.dart │ │ ├── price.dart │ │ ├── rating.dart │ │ ├── restriction.dart │ │ ├── rights.dart │ │ ├── scene.dart │ │ ├── star_rating.dart │ │ ├── statistics.dart │ │ ├── status.dart │ │ ├── tags.dart │ │ ├── text.dart │ │ ├── thumbnail.dart │ │ └── title.dart │ ├── rss_category.dart │ ├── rss_cloud.dart │ ├── rss_content.dart │ ├── rss_enclosure.dart │ ├── rss_feed.dart │ ├── rss_image.dart │ ├── rss_item.dart │ ├── rss_source.dart │ └── syndication │ │ └── syndication.dart ├── util │ ├── datetime.dart │ ├── function.dart │ ├── iterable.dart │ └── xml.dart └── webfeed.dart ├── pubspec.yaml └── test ├── atom_test.dart ├── rss_test.dart └── xml ├── Atom-Empty.xml ├── Atom-Media.xml ├── Atom.xml ├── Invalid.xml ├── RSS-DC.xml ├── RSS-Empty.xml ├── RSS-Itunes.xml ├── RSS-Itunes_item_empty_field.xml ├── RSS-Media.xml ├── RSS-RDF.xml ├── RSS-Syndication.xml └── RSS.xml /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: google/dart:latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install dependencies 17 | run: pub get 18 | - name: Run tests 19 | run: pub run test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .packages 5 | .pub/ 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | .idea 15 | .dart_tool 16 | 17 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart: 3 | - dev 4 | dart_task: 5 | - test: --platform vm --reporter expanded 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.7.0](https://pub.dartlang.org/packages/webfeed/versions/0.7.0) 4 | - Null safety migration [#50](https://github.com/witochandra/webfeed/pull/50) 5 | - Parse duration if not empty [#39](https://github.com/witochandra/webfeed/pull/39) 6 | 7 | ## [0.6.0](https://pub.dartlang.org/packages/webfeed/versions/0.6.0) 8 | - Refactor util/xml.dart 9 | - Support RDF feed 10 | - Support Syndication namespace 11 | 12 | ## [0.5.2](https://pub.dartlang.org/packages/webfeed/versions/0.5.2) 13 | - Lower the xml package version constraints 14 | 15 | ## [0.5.1](https://pub.dartlang.org/packages/webfeed/versions/0.5.1) 16 | - Support iTunes namespace [#19](https://github.com/witochandra/webfeed/pull/19) 17 | - Parse date strings into DateTime [#22](https://github.com/witochandra/webfeed/pull/22) 18 | - Add created & modified into dublin core namespace [#27](https://github.com/witochandra/webfeed/pull/27) 19 | - Upgrade xml package [#28](https://github.com/witochandra/webfeed/issues/28) 20 | - Fix linting warnings 21 | 22 | ## [0.4.2](https://pub.dartlang.org/packages/webfeed/versions/0.4.2) 23 | ### Fixed 24 | - Bad import in `rss_content.dart` & `rss_source.dart` 25 | 26 | ## [0.4.1](https://pub.dartlang.org/packages/webfeed/versions/0.4.1) 27 | ### Added 28 | - Support `author` in RssFeed 29 | 30 | ## [0.4.0](https://pub.dartlang.org/packages/webfeed/versions/0.4.0) 31 | ### Added 32 | - Support for dublin core namespace 33 | - Support enclosure in rss item 34 | - Set minimum dart version into 2 35 | 36 | ## [0.3.0](https://pub.dartlang.org/packages/webfeed/versions/0.3.0) 37 | ### Added 38 | - Support for image namespace 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wito Chandra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebFeed 2 | 3 | [![Build Status](https://travis-ci.org/witochandra/webfeed.svg?branch=master)](https://travis-ci.org/witochandra/webfeed) 4 | [![Pub](https://img.shields.io/pub/v/webfeed.svg)](https://pub.dartlang.org/packages/webfeed) 5 | 6 | A dart package for parsing RSS and Atom feed. 7 | 8 | ### Features 9 | 10 | - [x] RSS (0.9, 1.0, & 2.0) 11 | - [x] Atom 12 | - [x] Namespaces 13 | - [x] Media RSS 14 | - [x] Dublin Core 15 | - [x] iTunes 16 | - [x] Syndication 17 | 18 | ### Installing 19 | 20 | Add this line into your `pubspec.yaml` 21 | ``` 22 | webfeed: ^0.7.0 23 | ``` 24 | 25 | Import the package into your dart code using: 26 | ``` 27 | import 'package:webfeed/webfeed.dart'; 28 | ``` 29 | 30 | ### Example 31 | 32 | To parse string into `RssFeed` object use: 33 | ``` 34 | var rssFeed = RssFeed.parse(xmlString); // for parsing RSS feed 35 | var atomFeed = AtomFeed.parse(xmlString); // for parsing Atom feed 36 | ``` 37 | 38 | ### Preview 39 | 40 | **RSS** 41 | ``` 42 | feed.title 43 | feed.description 44 | feed.link 45 | feed.author 46 | feed.items 47 | feed.image 48 | feed.cloud 49 | feed.categories 50 | feed.skipDays 51 | feed.skipHours 52 | feed.lastBuildDate 53 | feed.language 54 | feed.generator 55 | feed.copyright 56 | feed.docs 57 | feed.managingEditor 58 | feed.rating 59 | feed.webMaster 60 | feed.ttl 61 | feed.dc 62 | 63 | RssItem item = feed.items.first; 64 | item.title 65 | item.description 66 | item.link 67 | item.categories 68 | item.guid 69 | item.pubDate 70 | item.author 71 | item.comments 72 | item.source 73 | item.media 74 | item.enclosure 75 | item.dc 76 | ``` 77 | 78 | **Atom** 79 | ``` 80 | feed.id 81 | feed.title 82 | feed.updated 83 | feed.items 84 | feed.links 85 | feed.authors 86 | feed.contributors 87 | feed.categories 88 | feed.generator 89 | feed.icon 90 | feed.logo 91 | feed.rights 92 | feed.subtitle 93 | 94 | AtomItem item = feed.items.first; 95 | item.id 96 | item.title 97 | item.updated 98 | item.authors 99 | item.links 100 | item.categories 101 | item.contributors 102 | item.source 103 | item.published 104 | item.content 105 | item.summary 106 | item.rights 107 | item.media 108 | ``` 109 | 110 | ## License 111 | 112 | WebFeed is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 113 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | errors: 3 | unused_import: error 4 | unused_local_variable: error 5 | dead_code: error 6 | linter: 7 | rules: 8 | # Errors 9 | - avoid_empty_else 10 | - comment_references 11 | - control_flow_in_finally 12 | - empty_statements 13 | - hash_and_equals 14 | - test_types_in_equals 15 | - throw_in_finally 16 | - unrelated_type_equality_checks 17 | - valid_regexps 18 | 19 | # Style 20 | - avoid_init_to_null 21 | - avoid_return_types_on_setters 22 | - await_only_futures 23 | - camel_case_types 24 | - directives_ordering 25 | - empty_constructor_bodies 26 | - library_names 27 | - library_prefixes 28 | - non_constant_identifier_names 29 | - omit_local_variable_types 30 | - prefer_final_fields 31 | - prefer_is_not_empty 32 | - prefer_typing_uninitialized_variables 33 | - slash_for_doc_comments 34 | - type_init_formals 35 | - unnecessary_new 36 | - prefer_single_quotes 37 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:http/io_client.dart'; 4 | import 'package:webfeed/webfeed.dart'; 5 | 6 | void main() async { 7 | final client = IOClient(HttpClient() 8 | ..badCertificateCallback = 9 | ((X509Certificate cert, String host, int port) => true)); 10 | 11 | // RSS feed 12 | var response = await client.get( 13 | Uri.parse('https://developer.apple.com/news/releases/rss/releases.rss')); 14 | var channel = RssFeed.parse(response.body); 15 | print(channel); 16 | 17 | // Atom feed 18 | response = 19 | await client.get(Uri.parse('https://www.theverge.com/rss/index.xml')); 20 | var feed = AtomFeed.parse(response.body); 21 | print(feed); 22 | 23 | client.close(); 24 | } 25 | -------------------------------------------------------------------------------- /lib/domain/atom_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class AtomCategory { 4 | final String? term; 5 | final String? scheme; 6 | final String? label; 7 | 8 | AtomCategory(this.term, this.scheme, this.label); 9 | 10 | factory AtomCategory.parse(XmlElement element) { 11 | var term = element.getAttribute('term'); 12 | var scheme = element.getAttribute('scheme'); 13 | var label = element.getAttribute('label'); 14 | return AtomCategory(term, scheme, label); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/atom_feed.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/atom_category.dart'; 2 | import 'package:webfeed/domain/atom_generator.dart'; 3 | import 'package:webfeed/domain/atom_item.dart'; 4 | import 'package:webfeed/domain/atom_link.dart'; 5 | import 'package:webfeed/domain/atom_person.dart'; 6 | import 'package:webfeed/util/datetime.dart'; 7 | import 'package:webfeed/util/iterable.dart'; 8 | import 'package:xml/xml.dart'; 9 | 10 | class AtomFeed { 11 | final String? id; 12 | final String? title; 13 | final DateTime? updated; 14 | final List? items; 15 | 16 | final List? links; 17 | final List? authors; 18 | final List? contributors; 19 | final List? categories; 20 | final AtomGenerator? generator; 21 | final String? icon; 22 | final String? logo; 23 | final String? rights; 24 | final String? subtitle; 25 | 26 | AtomFeed({ 27 | this.id, 28 | this.title, 29 | this.updated, 30 | this.items, 31 | this.links, 32 | this.authors, 33 | this.contributors, 34 | this.categories, 35 | this.generator, 36 | this.icon, 37 | this.logo, 38 | this.rights, 39 | this.subtitle, 40 | }); 41 | 42 | factory AtomFeed.parse(String xmlString) { 43 | var document = XmlDocument.parse(xmlString); 44 | var feedElement = document.findElements('feed').firstOrNull; 45 | if (feedElement == null) { 46 | throw ArgumentError('feed not found'); 47 | } 48 | 49 | return AtomFeed( 50 | id: feedElement.findElements('id').firstOrNull?.text, 51 | title: feedElement.findElements('title').firstOrNull?.text, 52 | updated: 53 | parseDateTime(feedElement.findElements('updated').firstOrNull?.text), 54 | items: feedElement 55 | .findElements('entry') 56 | .map((e) => AtomItem.parse(e)) 57 | .toList(), 58 | links: feedElement 59 | .findElements('link') 60 | .map((e) => AtomLink.parse(e)) 61 | .toList(), 62 | authors: feedElement 63 | .findElements('author') 64 | .map((e) => AtomPerson.parse(e)) 65 | .toList(), 66 | contributors: feedElement 67 | .findElements('contributor') 68 | .map((e) => AtomPerson.parse(e)) 69 | .toList(), 70 | categories: feedElement 71 | .findElements('category') 72 | .map((e) => AtomCategory.parse(e)) 73 | .toList(), 74 | generator: feedElement 75 | .findElements('generator') 76 | .map((e) => AtomGenerator.parse(e)) 77 | .firstOrNull, 78 | icon: feedElement.findElements('icon').firstOrNull?.text, 79 | logo: feedElement.findElements('logo').firstOrNull?.text, 80 | rights: feedElement.findElements('rights').firstOrNull?.text, 81 | subtitle: feedElement.findElements('subtitle').firstOrNull?.text, 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/domain/atom_generator.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class AtomGenerator { 4 | final String? uri; 5 | final String? version; 6 | final String? value; 7 | 8 | AtomGenerator(this.uri, this.version, this.value); 9 | 10 | factory AtomGenerator.parse(XmlElement element) { 11 | var uri = element.getAttribute('uri'); 12 | var version = element.getAttribute('version'); 13 | var value = element.text; 14 | return AtomGenerator(uri, version, value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/atom_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/atom_category.dart'; 2 | import 'package:webfeed/domain/atom_link.dart'; 3 | import 'package:webfeed/domain/atom_person.dart'; 4 | import 'package:webfeed/domain/atom_source.dart'; 5 | import 'package:webfeed/domain/media/media.dart'; 6 | import 'package:webfeed/util/datetime.dart'; 7 | import 'package:webfeed/util/iterable.dart'; 8 | import 'package:xml/xml.dart'; 9 | 10 | class AtomItem { 11 | final String? id; 12 | final String? title; 13 | final DateTime? updated; 14 | 15 | final List? authors; 16 | final List? links; 17 | final List? categories; 18 | final List? contributors; 19 | final AtomSource? source; 20 | final String? published; 21 | final String? content; 22 | final String? summary; 23 | final String? rights; 24 | final Media? media; 25 | 26 | AtomItem({ 27 | this.id, 28 | this.title, 29 | this.updated, 30 | this.authors, 31 | this.links, 32 | this.categories, 33 | this.contributors, 34 | this.source, 35 | this.published, 36 | this.content, 37 | this.summary, 38 | this.rights, 39 | this.media, 40 | }); 41 | 42 | factory AtomItem.parse(XmlElement element) { 43 | return AtomItem( 44 | id: element.findElements('id').firstOrNull?.text, 45 | title: element.findElements('title').firstOrNull?.text, 46 | updated: parseDateTime(element.findElements('updated').firstOrNull?.text), 47 | authors: element 48 | .findElements('author') 49 | .map((e) => AtomPerson.parse(e)) 50 | .toList(), 51 | links: 52 | element.findElements('link').map((e) => AtomLink.parse(e)).toList(), 53 | categories: element 54 | .findElements('category') 55 | .map((e) => AtomCategory.parse(e)) 56 | .toList(), 57 | contributors: element 58 | .findElements('contributor') 59 | .map((e) => AtomPerson.parse(e)) 60 | .toList(), 61 | source: element 62 | .findElements('source') 63 | .map((e) => AtomSource.parse(e)) 64 | .firstOrNull, 65 | published: element.findElements('published').firstOrNull?.text, 66 | content: element.findElements('content').firstOrNull?.text, 67 | summary: element.findElements('summary').firstOrNull?.text, 68 | rights: element.findElements('rights').firstOrNull?.text, 69 | media: Media.parse(element), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/domain/atom_link.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class AtomLink { 4 | final String? href; 5 | final String? rel; 6 | final String? type; 7 | final String? hreflang; 8 | final String? title; 9 | final int length; 10 | 11 | AtomLink( 12 | this.href, 13 | this.rel, 14 | this.type, 15 | this.hreflang, 16 | this.title, 17 | this.length, 18 | ); 19 | 20 | factory AtomLink.parse(XmlElement element) { 21 | var href = element.getAttribute('href'); 22 | var rel = element.getAttribute('rel'); 23 | var type = element.getAttribute('type'); 24 | var title = element.getAttribute('title'); 25 | var hreflang = element.getAttribute('hreflang'); 26 | var length = 0; 27 | if (element.getAttribute('length') != null) { 28 | length = int.parse(element.getAttribute('length')!); 29 | } 30 | return AtomLink(href, rel, type, hreflang, title, length); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/domain/atom_person.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/iterable.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class AtomPerson { 5 | final String? name; 6 | final String? uri; 7 | final String? email; 8 | 9 | AtomPerson({this.name, this.uri, this.email}); 10 | 11 | factory AtomPerson.parse(XmlElement element) { 12 | return AtomPerson( 13 | name: element.findElements('name').firstOrNull?.text, 14 | uri: element.findElements('uri').firstOrNull?.text, 15 | email: element.findElements('email').firstOrNull?.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/atom_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/iterable.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class AtomSource { 5 | final String? id; 6 | final String? title; 7 | final String? updated; 8 | 9 | AtomSource({ 10 | this.id, 11 | this.title, 12 | this.updated, 13 | }); 14 | 15 | factory AtomSource.parse(XmlElement element) { 16 | return AtomSource( 17 | id: element.findElements('id').firstOrNull?.text, 18 | title: element.findElements('title').firstOrNull?.text, 19 | updated: element.findElements('updated').firstOrNull?.text, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/domain/dublin_core/dublin_core.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/datetime.dart'; 2 | import 'package:webfeed/util/iterable.dart'; 3 | import 'package:xml/xml.dart'; 4 | 5 | class DublinCore { 6 | final String? title; 7 | final String? description; 8 | final String? creator; 9 | final String? subject; 10 | final String? publisher; 11 | final String? contributor; 12 | final DateTime? date; 13 | final DateTime? created; 14 | final DateTime? modified; 15 | final String? type; 16 | final String? format; 17 | final String? identifier; 18 | final String? source; 19 | final String? language; 20 | final String? relation; 21 | final String? coverage; 22 | final String? rights; 23 | 24 | DublinCore({ 25 | this.title, 26 | this.description, 27 | this.creator, 28 | this.subject, 29 | this.publisher, 30 | this.contributor, 31 | this.date, 32 | this.created, 33 | this.modified, 34 | this.type, 35 | this.format, 36 | this.identifier, 37 | this.source, 38 | this.language, 39 | this.relation, 40 | this.coverage, 41 | this.rights, 42 | }); 43 | 44 | factory DublinCore.parse(XmlElement element) { 45 | return DublinCore( 46 | title: element.findElements('dc:title').firstOrNull?.text, 47 | description: element.findElements('dc:description').firstOrNull?.text, 48 | creator: element.findElements('dc:creator').firstOrNull?.text, 49 | subject: element.findElements('dc:subject').firstOrNull?.text, 50 | publisher: element.findElements('dc:publisher').firstOrNull?.text, 51 | contributor: element.findElements('dc:contributor').firstOrNull?.text, 52 | date: parseDateTime(element.findElements('dc:date').firstOrNull?.text), 53 | created: 54 | parseDateTime(element.findElements('dc:created').firstOrNull?.text), 55 | modified: 56 | parseDateTime(element.findElements('dc:modified').firstOrNull?.text), 57 | type: element.findElements('dc:type').firstOrNull?.text, 58 | format: element.findElements('dc:format').firstOrNull?.text, 59 | identifier: element.findElements('dc:identifier').firstOrNull?.text, 60 | source: element.findElements('dc:source').firstOrNull?.text, 61 | language: element.findElements('dc:language').firstOrNull?.text, 62 | relation: element.findElements('dc:relation').firstOrNull?.text, 63 | coverage: element.findElements('dc:coverage').firstOrNull?.text, 64 | rights: element.findElements('dc:rights').firstOrNull?.text, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/itunes/itunes_category.dart'; 2 | import 'package:webfeed/domain/itunes/itunes_episode_type.dart'; 3 | import 'package:webfeed/domain/itunes/itunes_image.dart'; 4 | import 'package:webfeed/domain/itunes/itunes_owner.dart'; 5 | import 'package:webfeed/domain/itunes/itunes_type.dart'; 6 | import 'package:webfeed/util/iterable.dart'; 7 | import 'package:webfeed/util/xml.dart'; 8 | import 'package:xml/xml.dart'; 9 | 10 | class Itunes { 11 | final String? author; 12 | final String? summary; 13 | final bool? explicit; 14 | final String? title; 15 | final String? subtitle; 16 | final ItunesOwner? owner; 17 | final List? keywords; 18 | final ItunesImage? image; 19 | final List? categories; 20 | final ItunesType? type; 21 | final String? newFeedUrl; 22 | final bool? block; 23 | final bool? complete; 24 | final int? episode; 25 | final int? season; 26 | final Duration? duration; 27 | final ItunesEpisodeType? episodeType; 28 | 29 | Itunes({ 30 | this.author, 31 | this.summary, 32 | this.explicit, 33 | this.title, 34 | this.subtitle, 35 | this.owner, 36 | this.keywords, 37 | this.image, 38 | this.categories, 39 | this.type, 40 | this.newFeedUrl, 41 | this.block, 42 | this.complete, 43 | this.episode, 44 | this.season, 45 | this.duration, 46 | this.episodeType, 47 | }); 48 | 49 | factory Itunes.parse(XmlElement element) { 50 | final episodeStr = 51 | element.findElements('itunes:episode').firstOrNull?.text ?? ''; 52 | final seasonStr = 53 | element.findElements('itunes:season').firstOrNull?.text ?? ''; 54 | final durationStr = 55 | element.findElements('itunes:duration').firstOrNull?.text ?? ''; 56 | return Itunes( 57 | author: element.findElements('itunes:author').firstOrNull?.text, 58 | summary: element.findElements('itunes:summary').firstOrNull?.text, 59 | explicit: parseBoolLiteral(element, 'itunes:explicit'), 60 | title: element.findElements('itunes:title').firstOrNull?.text, 61 | subtitle: element.findElements('itunes:subtitle').firstOrNull?.text, 62 | owner: element 63 | .findElements('itunes:owner') 64 | .map((e) => ItunesOwner.parse(e)) 65 | .firstOrNull, 66 | keywords: element 67 | .findElements('itunes:keywords') 68 | .firstOrNull 69 | ?.text 70 | .split(',') 71 | .map((keyword) => keyword.trim()) 72 | .toList() ?? 73 | [], 74 | image: element 75 | .findElements('itunes:image') 76 | .map((e) => ItunesImage.parse(e)) 77 | .firstOrNull, 78 | categories: element 79 | .findElements('itunes:category') 80 | .map((e) => ItunesCategory.parse(e)) 81 | .toList(), 82 | type: element 83 | .findElements('itunes:type') 84 | .map((e) => newItunesType(e)) 85 | .firstOrNull, 86 | newFeedUrl: element.findElements('itunes:new-feed-url').firstOrNull?.text, 87 | block: parseBoolLiteral(element, 'itunes:block'), 88 | complete: parseBoolLiteral(element, 'itunes:complete'), 89 | episode: episodeStr.isNotEmpty ? int.tryParse(episodeStr) : null, 90 | season: seasonStr.isNotEmpty ? int.tryParse(seasonStr) : null, 91 | duration: durationStr.isNotEmpty ? _parseDuration(durationStr) : null, 92 | episodeType: element 93 | .findElements('itunes:episodeType') 94 | .map((e) => newItunesEpisodeType(e)) 95 | .firstOrNull, 96 | ); 97 | } 98 | 99 | static Duration _parseDuration(String s) { 100 | var hours = 0; 101 | var minutes = 0; 102 | var seconds = 0; 103 | var parts = s.split(':'); 104 | if (parts.length > 2) { 105 | hours = int.tryParse(parts[parts.length - 3]) ?? 0; 106 | } 107 | if (parts.length > 1) { 108 | minutes = int.tryParse(parts[parts.length - 2]) ?? 0; 109 | } 110 | seconds = int.tryParse(parts[parts.length - 1]) ?? 0; 111 | return Duration( 112 | hours: hours, 113 | minutes: minutes, 114 | seconds: seconds, 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class ItunesCategory { 4 | final String? category; 5 | final List? subCategories; 6 | 7 | ItunesCategory({this.category, this.subCategories}); 8 | 9 | factory ItunesCategory.parse(XmlElement element) { 10 | return ItunesCategory( 11 | category: element.getAttribute('text')?.trim(), 12 | subCategories: element 13 | .findElements('itunes:category') 14 | .map((e) => e.getAttribute('text')?.trim() ?? '') 15 | .toList()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes_episode_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | enum ItunesEpisodeType { full, trailer, bonus, unknown } 4 | 5 | ItunesEpisodeType newItunesEpisodeType(XmlElement element) { 6 | switch (element.text) { 7 | case 'full': 8 | return ItunesEpisodeType.full; 9 | case 'trailer': 10 | return ItunesEpisodeType.trailer; 11 | case 'bonus': 12 | return ItunesEpisodeType.bonus; 13 | default: 14 | return ItunesEpisodeType.unknown; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class ItunesImage { 4 | final String? href; 5 | 6 | ItunesImage({this.href}); 7 | 8 | factory ItunesImage.parse(XmlElement element) { 9 | return ItunesImage( 10 | href: element.getAttribute('href')?.trim(), 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes_owner.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/iterable.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class ItunesOwner { 5 | final String? name; 6 | final String? email; 7 | 8 | ItunesOwner({this.name, this.email}); 9 | 10 | factory ItunesOwner.parse(XmlElement element) { 11 | return ItunesOwner( 12 | name: element.findElements('itunes:name').firstOrNull?.text.trim(), 13 | email: element.findElements('itunes:email').firstOrNull?.text.trim(), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/itunes/itunes_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | enum ItunesType { episodic, serial, unknown } 4 | 5 | ItunesType newItunesType(XmlElement element) { 6 | switch (element.text) { 7 | case 'episodic': 8 | return ItunesType.episodic; 9 | case 'serial': 10 | return ItunesType.serial; 11 | default: 12 | return ItunesType.unknown; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/domain/media/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Category { 4 | final String? scheme; 5 | final String? label; 6 | final String? value; 7 | 8 | Category({ 9 | this.scheme, 10 | this.label, 11 | this.value, 12 | }); 13 | 14 | factory Category.parse(XmlElement element) { 15 | return Category( 16 | scheme: element.getAttribute('scheme'), 17 | label: element.getAttribute('label'), 18 | value: element.text, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/media/community.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/media/star_rating.dart'; 2 | import 'package:webfeed/domain/media/statistics.dart'; 3 | import 'package:webfeed/domain/media/tags.dart'; 4 | import 'package:webfeed/util/iterable.dart'; 5 | import 'package:xml/xml.dart'; 6 | 7 | class Community { 8 | final StarRating? starRating; 9 | final Statistics? statistics; 10 | final Tags? tags; 11 | 12 | Community({ 13 | this.starRating, 14 | this.statistics, 15 | this.tags, 16 | }); 17 | 18 | factory Community.parse(XmlElement element) { 19 | return Community( 20 | starRating: element 21 | .findElements('media:starRating') 22 | .map((e) => StarRating.parse(e)) 23 | .firstOrNull, 24 | statistics: element 25 | .findElements('media:statistics') 26 | .map((e) => Statistics.parse(e)) 27 | .firstOrNull, 28 | tags: element 29 | .findElements('media:tags') 30 | .map((e) => Tags.parse(e)) 31 | .firstOrNull, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/domain/media/content.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Content { 4 | final String? url; 5 | final String? type; 6 | final int? fileSize; 7 | final String? medium; 8 | final bool? isDefault; 9 | final String? expression; 10 | final int? bitrate; 11 | final double? framerate; 12 | final double? samplingrate; 13 | final int? channels; 14 | final int? duration; 15 | final int? height; 16 | final int? width; 17 | final String? lang; 18 | 19 | Content({ 20 | this.url, 21 | this.type, 22 | this.fileSize, 23 | this.medium, 24 | this.isDefault, 25 | this.expression, 26 | this.bitrate, 27 | this.framerate, 28 | this.samplingrate, 29 | this.channels, 30 | this.duration, 31 | this.height, 32 | this.width, 33 | this.lang, 34 | }); 35 | 36 | factory Content.parse(XmlElement element) { 37 | return Content( 38 | url: element.getAttribute('url'), 39 | type: element.getAttribute('type'), 40 | fileSize: int.tryParse(element.getAttribute('fileSize') ?? '0'), 41 | medium: element.getAttribute('medium'), 42 | isDefault: element.getAttribute('isDefault') == 'true', 43 | expression: element.getAttribute('expression'), 44 | bitrate: int.tryParse(element.getAttribute('bitrate') ?? '0'), 45 | framerate: double.tryParse(element.getAttribute('framerate') ?? '0'), 46 | samplingrate: double.tryParse( 47 | element.getAttribute('samplingrate') ?? '0', 48 | ), 49 | channels: int.tryParse(element.getAttribute('channels') ?? '0'), 50 | duration: int.tryParse(element.getAttribute('duration') ?? '0'), 51 | height: int.tryParse(element.getAttribute('height') ?? '0'), 52 | width: int.tryParse(element.getAttribute('width') ?? '0'), 53 | lang: element.getAttribute('lang'), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/domain/media/copyright.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Copyright { 4 | final String? url; 5 | final String? value; 6 | 7 | Copyright({ 8 | this.url, 9 | this.value, 10 | }); 11 | 12 | factory Copyright.parse(XmlElement element) { 13 | return Copyright( 14 | url: element.getAttribute('url'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/credit.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Credit { 4 | final String? role; 5 | final String? scheme; 6 | final String? value; 7 | 8 | Credit({ 9 | this.role, 10 | this.scheme, 11 | this.value, 12 | }); 13 | 14 | factory Credit.parse(XmlElement element) { 15 | return Credit( 16 | role: element.getAttribute('role'), 17 | scheme: element.getAttribute('scheme'), 18 | value: element.text, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/media/description.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Description { 4 | final String? type; 5 | final String? value; 6 | 7 | Description({ 8 | this.type, 9 | this.value, 10 | }); 11 | 12 | factory Description.parse(XmlElement element) { 13 | return Description( 14 | type: element.getAttribute('type'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/embed.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/media/param.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class Embed { 5 | final String? url; 6 | final int? width; 7 | final int? height; 8 | final List? params; 9 | 10 | Embed({ 11 | this.url, 12 | this.width, 13 | this.height, 14 | this.params, 15 | }); 16 | 17 | factory Embed.parse(XmlElement element) { 18 | return Embed( 19 | url: element.getAttribute('url'), 20 | width: int.tryParse(element.getAttribute('width') ?? '0'), 21 | height: int.tryParse(element.getAttribute('height') ?? '0'), 22 | params: element 23 | .findElements('media:param') 24 | .map((e) => Param.parse(e)) 25 | .toList(), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/domain/media/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/media/category.dart'; 2 | import 'package:webfeed/domain/media/content.dart'; 3 | import 'package:webfeed/domain/media/credit.dart'; 4 | import 'package:webfeed/domain/media/rating.dart'; 5 | import 'package:webfeed/util/iterable.dart'; 6 | import 'package:xml/xml.dart'; 7 | 8 | class Group { 9 | final List? contents; 10 | final List? credits; 11 | final Category? category; 12 | final Rating? rating; 13 | 14 | Group({ 15 | this.contents, 16 | this.credits, 17 | this.category, 18 | this.rating, 19 | }); 20 | 21 | factory Group.parse(XmlElement element) { 22 | return Group( 23 | contents: element 24 | .findElements('media:content') 25 | .map((e) => Content.parse(e)) 26 | .toList(), 27 | credits: element 28 | .findElements('media:credit') 29 | .map((e) => Credit.parse(e)) 30 | .toList(), 31 | category: element 32 | .findElements('media:category') 33 | .map((e) => Category.parse(e)) 34 | .firstOrNull, 35 | rating: element 36 | .findElements('media:rating') 37 | .map((e) => Rating.parse(e)) 38 | .firstOrNull, 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/domain/media/hash.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Hash { 4 | final String? algo; 5 | final String? value; 6 | 7 | Hash({ 8 | this.algo, 9 | this.value, 10 | }); 11 | 12 | factory Hash.parse(XmlElement element) { 13 | return Hash( 14 | algo: element.getAttribute('algo'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/license.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class License { 4 | final String? type; 5 | final String? href; 6 | final String? value; 7 | 8 | License({ 9 | this.type, 10 | this.href, 11 | this.value, 12 | }); 13 | 14 | factory License.parse(XmlElement element) { 15 | return License( 16 | type: element.getAttribute('type'), 17 | href: element.getAttribute('href'), 18 | value: element.text, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/media/media.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/media/category.dart'; 2 | import 'package:webfeed/domain/media/community.dart'; 3 | import 'package:webfeed/domain/media/content.dart'; 4 | import 'package:webfeed/domain/media/copyright.dart'; 5 | import 'package:webfeed/domain/media/credit.dart'; 6 | import 'package:webfeed/domain/media/description.dart'; 7 | import 'package:webfeed/domain/media/embed.dart'; 8 | import 'package:webfeed/domain/media/group.dart'; 9 | import 'package:webfeed/domain/media/hash.dart'; 10 | import 'package:webfeed/domain/media/license.dart'; 11 | import 'package:webfeed/domain/media/peer_link.dart'; 12 | import 'package:webfeed/domain/media/player.dart'; 13 | import 'package:webfeed/domain/media/price.dart'; 14 | import 'package:webfeed/domain/media/rating.dart'; 15 | import 'package:webfeed/domain/media/restriction.dart'; 16 | import 'package:webfeed/domain/media/rights.dart'; 17 | import 'package:webfeed/domain/media/scene.dart'; 18 | import 'package:webfeed/domain/media/status.dart'; 19 | import 'package:webfeed/domain/media/text.dart'; 20 | import 'package:webfeed/domain/media/thumbnail.dart'; 21 | import 'package:webfeed/domain/media/title.dart'; 22 | import 'package:webfeed/util/xml.dart'; 23 | import 'package:webfeed/util/iterable.dart'; 24 | import 'package:xml/xml.dart'; 25 | 26 | class Media { 27 | final Group? group; 28 | final List? contents; 29 | final List? credits; 30 | final Category? category; 31 | final Rating? rating; 32 | final Title? title; 33 | final Description? description; 34 | final String? keywords; 35 | final List? thumbnails; 36 | final Hash? hash; 37 | final Player? player; 38 | final Copyright? copyright; 39 | final Text? text; 40 | final Restriction? restriction; 41 | final Community? community; 42 | final List? comments; 43 | final Embed? embed; 44 | final List? responses; 45 | final List? backLinks; 46 | final Status? status; 47 | final List? prices; 48 | final License? license; 49 | final PeerLink? peerLink; 50 | final Rights? rights; 51 | final List? scenes; 52 | 53 | Media({ 54 | this.group, 55 | this.contents, 56 | this.credits, 57 | this.category, 58 | this.rating, 59 | this.title, 60 | this.description, 61 | this.keywords, 62 | this.thumbnails, 63 | this.hash, 64 | this.player, 65 | this.copyright, 66 | this.text, 67 | this.restriction, 68 | this.community, 69 | this.comments, 70 | this.embed, 71 | this.responses, 72 | this.backLinks, 73 | this.status, 74 | this.prices, 75 | this.license, 76 | this.peerLink, 77 | this.rights, 78 | this.scenes, 79 | }); 80 | 81 | factory Media.parse(XmlElement element) { 82 | return Media( 83 | group: element 84 | .findElements('media:group') 85 | .map((e) => Group.parse(e)) 86 | .firstOrNull, 87 | contents: element 88 | .findElements('media:content') 89 | .map((e) => Content.parse(e)) 90 | .toList(), 91 | credits: element 92 | .findElements('media:credit') 93 | .map((e) => Credit.parse(e)) 94 | .toList(), 95 | category: element 96 | .findElements('media:category') 97 | .map((e) => Category.parse(e)) 98 | .firstOrNull, 99 | rating: element 100 | .findElements('media:rating') 101 | .map((e) => Rating.parse(e)) 102 | .firstOrNull, 103 | title: findElements(element, 'media:title') 104 | ?.map((e) => Title.parse(e)) 105 | .firstOrNull, 106 | description: element 107 | .findElements('media:description') 108 | .map((e) => Description.parse(e)) 109 | .firstOrNull, 110 | keywords: element.findElements('media:keywords').firstOrNull?.text, 111 | thumbnails: element 112 | .findElements('media:thumbnail') 113 | .map((e) => Thumbnail.parse(e)) 114 | .toList(), 115 | hash: element 116 | .findElements('media:hash') 117 | .map((e) => Hash.parse(e)) 118 | .firstOrNull, 119 | player: element 120 | .findElements('media:player') 121 | .map((e) => Player.parse(e)) 122 | .firstOrNull, 123 | copyright: element 124 | .findElements('media:copyright') 125 | .map((e) => Copyright.parse(e)) 126 | .firstOrNull, 127 | text: element 128 | .findElements('media:text') 129 | .map((e) => Text.parse(e)) 130 | .firstOrNull, 131 | restriction: element 132 | .findElements('media:restriction') 133 | .map((e) => Restriction.parse(e)) 134 | .firstOrNull, 135 | community: element 136 | .findElements('media:community') 137 | .map((e) => Community.parse(e)) 138 | .firstOrNull, 139 | comments: element 140 | .findElements('media:comments') 141 | .firstOrNull 142 | ?.findElements('media:comment') 143 | .map((e) => e.text) 144 | .toList() ?? 145 | [], 146 | embed: element 147 | .findElements('media:embed') 148 | .map((e) => Embed.parse(e)) 149 | .firstOrNull, 150 | responses: element 151 | .findElements('media:responses') 152 | .firstOrNull 153 | ?.findElements('media:response') 154 | .map((e) => e.text) 155 | .toList() ?? 156 | [], 157 | backLinks: element 158 | .findElements('media:backLinks') 159 | .firstOrNull 160 | ?.findElements('media:backLink') 161 | .map((e) => e.text) 162 | .toList() ?? 163 | [], 164 | status: element 165 | .findElements('media:status') 166 | .map((e) => Status.parse(e)) 167 | .firstOrNull, 168 | prices: element 169 | .findElements('media:price') 170 | .map((e) => Price.parse(e)) 171 | .toList(), 172 | license: element 173 | .findElements('media:license') 174 | .map((e) => License.parse(e)) 175 | .firstOrNull, 176 | peerLink: element 177 | .findElements('media:peerLink') 178 | .map((e) => PeerLink.parse(e)) 179 | .firstOrNull, 180 | rights: element 181 | .findElements('media:rights') 182 | .map((e) => Rights.parse(e)) 183 | .firstOrNull, 184 | scenes: element 185 | .findElements('media:scenes') 186 | .firstOrNull 187 | ?.findElements('media:scene') 188 | .map((e) => Scene.parse(e)) 189 | .toList() ?? 190 | [], 191 | ); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/domain/media/param.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Param { 4 | final String? name; 5 | final String? value; 6 | 7 | Param({ 8 | this.name, 9 | this.value, 10 | }); 11 | 12 | factory Param.parse(XmlElement element) { 13 | return Param( 14 | name: element.getAttribute('name'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/peer_link.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class PeerLink { 4 | final String? type; 5 | final String? href; 6 | final String? value; 7 | 8 | PeerLink({ 9 | this.type, 10 | this.href, 11 | this.value, 12 | }); 13 | 14 | factory PeerLink.parse(XmlElement element) { 15 | return PeerLink( 16 | type: element.getAttribute('type'), 17 | href: element.getAttribute('href'), 18 | value: element.text, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/media/player.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Player { 4 | final String? url; 5 | final int? width; 6 | final int? height; 7 | final String? value; 8 | 9 | Player({ 10 | this.url, 11 | this.width, 12 | this.height, 13 | this.value, 14 | }); 15 | 16 | factory Player.parse(XmlElement element) { 17 | return Player( 18 | url: element.getAttribute('url'), 19 | width: int.tryParse(element.getAttribute('width') ?? '0'), 20 | height: int.tryParse(element.getAttribute('height') ?? '0'), 21 | value: element.text, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/domain/media/price.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Price { 4 | final double? price; 5 | final String? type; 6 | final String? info; 7 | final String? currency; 8 | 9 | Price({ 10 | this.price, 11 | this.type, 12 | this.info, 13 | this.currency, 14 | }); 15 | 16 | factory Price.parse(XmlElement element) { 17 | return Price( 18 | price: double.tryParse(element.getAttribute('price') ?? '0'), 19 | type: element.getAttribute('type'), 20 | info: element.getAttribute('info'), 21 | currency: element.getAttribute('currency'), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/domain/media/rating.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Rating { 4 | final String? scheme; 5 | final String? value; 6 | 7 | Rating({ 8 | this.scheme, 9 | this.value, 10 | }); 11 | 12 | factory Rating.parse(XmlElement element) { 13 | return Rating( 14 | scheme: element.getAttribute('scheme'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/restriction.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Restriction { 4 | final String? relationship; 5 | final String? type; 6 | final String? value; 7 | 8 | Restriction({ 9 | this.relationship, 10 | this.type, 11 | this.value, 12 | }); 13 | 14 | factory Restriction.parse(XmlElement element) { 15 | return Restriction( 16 | relationship: element.getAttribute('relationship'), 17 | type: element.getAttribute('type'), 18 | value: element.text, 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/domain/media/rights.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Rights { 4 | final String? status; 5 | 6 | Rights({ 7 | this.status, 8 | }); 9 | 10 | factory Rights.parse(XmlElement element) { 11 | return Rights( 12 | status: element.getAttribute('status'), 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/media/scene.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/iterable.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class Scene { 5 | final String? title; 6 | final String? description; 7 | final String? startTime; 8 | final String? endTime; 9 | 10 | Scene({ 11 | this.title, 12 | this.description, 13 | this.startTime, 14 | this.endTime, 15 | }); 16 | 17 | factory Scene.parse(XmlElement element) { 18 | return Scene( 19 | title: element.findElements('sceneTitle').firstOrNull?.text, 20 | description: element.findElements('sceneDescription').firstOrNull?.text, 21 | startTime: element.findElements('sceneStartTime').firstOrNull?.text, 22 | endTime: element.findElements('sceneEndTime').firstOrNull?.text, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/domain/media/star_rating.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class StarRating { 4 | final double? average; 5 | final int? count; 6 | final int? min; 7 | final int? max; 8 | 9 | StarRating({ 10 | this.average, 11 | this.count, 12 | this.min, 13 | this.max, 14 | }); 15 | 16 | factory StarRating.parse(XmlElement? element) { 17 | return StarRating( 18 | average: double.tryParse(element?.getAttribute('average') ?? '0'), 19 | count: int.tryParse(element?.getAttribute('count') ?? '0'), 20 | min: int.tryParse(element?.getAttribute('min') ?? '0'), 21 | max: int.tryParse(element?.getAttribute('max') ?? '0'), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/domain/media/statistics.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Statistics { 4 | final int? views; 5 | final int? favorites; 6 | 7 | Statistics({ 8 | this.views, 9 | this.favorites, 10 | }); 11 | 12 | factory Statistics.parse(XmlElement? element) { 13 | return Statistics( 14 | views: int.tryParse(element?.getAttribute('views') ?? '0'), 15 | favorites: int.tryParse(element?.getAttribute('favorites') ?? '0'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/status.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Status { 4 | final String? state; 5 | final String? reason; 6 | 7 | Status({ 8 | this.state, 9 | this.reason, 10 | }); 11 | 12 | factory Status.parse(XmlElement element) { 13 | return Status( 14 | state: element.getAttribute('state'), 15 | reason: element.getAttribute('reason'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/tags.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Tags { 4 | final String? tags; 5 | final int? weight; 6 | 7 | Tags({ 8 | this.tags, 9 | this.weight, 10 | }); 11 | 12 | factory Tags.parse(XmlElement element) { 13 | return Tags( 14 | tags: element.text, 15 | weight: int.tryParse(element.getAttribute('weight') ?? '1'), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/media/text.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Text { 4 | final String? type; 5 | final String? lang; 6 | final String? start; 7 | final String? end; 8 | final String? value; 9 | 10 | Text({ 11 | this.type, 12 | this.lang, 13 | this.start, 14 | this.end, 15 | this.value, 16 | }); 17 | 18 | factory Text.parse(XmlElement element) { 19 | return Text( 20 | type: element.getAttribute('type'), 21 | lang: element.getAttribute('lang'), 22 | start: element.getAttribute('start'), 23 | end: element.getAttribute('end'), 24 | value: element.text, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/domain/media/thumbnail.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Thumbnail { 4 | final String? url; 5 | final String? width; 6 | final String? height; 7 | final String? time; 8 | 9 | Thumbnail({ 10 | this.url, 11 | this.width, 12 | this.height, 13 | this.time, 14 | }); 15 | 16 | factory Thumbnail.parse(XmlElement element) { 17 | return Thumbnail( 18 | url: element.getAttribute('url'), 19 | width: element.getAttribute('width'), 20 | height: element.getAttribute('height'), 21 | time: element.getAttribute('time'), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/domain/media/title.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class Title { 4 | final String? type; 5 | final String? value; 6 | 7 | Title({ 8 | this.type, 9 | this.value, 10 | }); 11 | 12 | factory Title.parse(XmlElement element) { 13 | return Title( 14 | type: element.getAttribute('type'), 15 | value: element.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/rss_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class RssCategory { 4 | final String? domain; 5 | final String value; 6 | 7 | RssCategory(this.domain, this.value); 8 | 9 | factory RssCategory.parse(XmlElement element) { 10 | var domain = element.getAttribute('domain'); 11 | var value = element.text; 12 | 13 | return RssCategory(domain, value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/rss_cloud.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class RssCloud { 4 | final String? domain; 5 | final String? port; 6 | final String? path; 7 | final String? registerProcedure; 8 | final String? protocol; 9 | 10 | RssCloud( 11 | this.domain, 12 | this.port, 13 | this.path, 14 | this.registerProcedure, 15 | this.protocol, 16 | ); 17 | 18 | factory RssCloud.parse(XmlElement node) { 19 | var domain = node.getAttribute('domain'); 20 | var port = node.getAttribute('port'); 21 | var path = node.getAttribute('path'); 22 | var registerProcedure = node.getAttribute('registerProcedure'); 23 | var protocol = node.getAttribute('protocol'); 24 | return RssCloud(domain, port, path, registerProcedure, protocol); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/domain/rss_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | final _imagesRegExp = RegExp( 4 | "]+)(?:'|\")", 5 | multiLine: true, 6 | caseSensitive: false, 7 | ); 8 | 9 | /// For RSS Content Module: 10 | /// 11 | /// - `xmlns:content="http://purl.org/rss/1.0/modules/content/"` 12 | /// 13 | class RssContent { 14 | String value; 15 | Iterable images; 16 | 17 | RssContent(this.value, this.images); 18 | 19 | factory RssContent.parse(XmlElement element) { 20 | final dynamic? content = element.text; 21 | final images = []; 22 | _imagesRegExp.allMatches(content).forEach((match) { 23 | images.add(match.group(1)!); 24 | }); 25 | return RssContent(content, images); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/domain/rss_enclosure.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class RssEnclosure { 4 | final String? url; 5 | final String? type; 6 | final int? length; 7 | 8 | RssEnclosure(this.url, this.type, this.length); 9 | 10 | factory RssEnclosure.parse(XmlElement element) { 11 | var url = element.getAttribute('url'); 12 | var type = element.getAttribute('type'); 13 | var length = int.tryParse(element.getAttribute('length') ?? '0'); 14 | return RssEnclosure(url, type, length); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/domain/rss_feed.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:webfeed/domain/dublin_core/dublin_core.dart'; 4 | import 'package:webfeed/domain/itunes/itunes.dart'; 5 | import 'package:webfeed/domain/rss_category.dart'; 6 | import 'package:webfeed/domain/rss_cloud.dart'; 7 | import 'package:webfeed/domain/rss_image.dart'; 8 | import 'package:webfeed/domain/rss_item.dart'; 9 | import 'package:webfeed/domain/syndication/syndication.dart'; 10 | import 'package:webfeed/util/iterable.dart'; 11 | import 'package:xml/xml.dart'; 12 | 13 | class RssFeed { 14 | final String? title; 15 | final String? author; 16 | final String? description; 17 | final String? link; 18 | final List? items; 19 | 20 | final RssImage? image; 21 | final RssCloud? cloud; 22 | final List? categories; 23 | final List? skipDays; 24 | final List? skipHours; 25 | final String? lastBuildDate; 26 | final String? language; 27 | final String? generator; 28 | final String? copyright; 29 | final String? docs; 30 | final String? managingEditor; 31 | final String? rating; 32 | final String? webMaster; 33 | final int? ttl; 34 | final DublinCore? dc; 35 | final Itunes? itunes; 36 | final Syndication? syndication; 37 | 38 | RssFeed({ 39 | this.title, 40 | this.author, 41 | this.description, 42 | this.link, 43 | this.items, 44 | this.image, 45 | this.cloud, 46 | this.categories, 47 | this.skipDays, 48 | this.skipHours, 49 | this.lastBuildDate, 50 | this.language, 51 | this.generator, 52 | this.copyright, 53 | this.docs, 54 | this.managingEditor, 55 | this.rating, 56 | this.webMaster, 57 | this.ttl, 58 | this.dc, 59 | this.itunes, 60 | this.syndication, 61 | }); 62 | 63 | factory RssFeed.parse(String xmlString) { 64 | var document = XmlDocument.parse(xmlString); 65 | var rss = document.findElements('rss').firstOrNull; 66 | var rdf = document.findElements('rdf:RDF').firstOrNull; 67 | if (rss == null && rdf == null) { 68 | throw ArgumentError('not a rss feed'); 69 | } 70 | var channelElement = (rss ?? rdf)!.findElements('channel').firstOrNull; 71 | if (channelElement == null) { 72 | throw ArgumentError('channel not found'); 73 | } 74 | return RssFeed( 75 | title: channelElement.findElements('title').firstOrNull?.text, 76 | author: channelElement.findElements('author').firstOrNull?.text, 77 | description: channelElement.findElements('description').firstOrNull?.text, 78 | link: channelElement.findElements('link').firstOrNull?.text, 79 | items: (rdf ?? channelElement) 80 | .findElements('item') 81 | .map((e) => RssItem.parse(e)) 82 | .toList(), 83 | image: (rdf ?? channelElement) 84 | .findElements('image') 85 | .map((e) => RssImage.parse(e)) 86 | .firstOrNull, 87 | cloud: channelElement 88 | .findElements('cloud') 89 | .map((e) => RssCloud.parse(e)) 90 | .firstOrNull, 91 | categories: channelElement 92 | .findElements('category') 93 | .map((e) => RssCategory.parse(e)) 94 | .toList(), 95 | skipDays: channelElement 96 | .findElements('skipDays') 97 | .firstOrNull 98 | ?.findAllElements('day') 99 | .map((e) => e.text) 100 | .toList() ?? 101 | [], 102 | skipHours: channelElement 103 | .findElements('skipHours') 104 | .firstOrNull 105 | ?.findAllElements('hour') 106 | .map((e) => int.tryParse(e.text) ?? 0) 107 | .toList() ?? 108 | [], 109 | lastBuildDate: 110 | channelElement.findElements('lastBuildDate').firstOrNull?.text, 111 | language: channelElement.findElements('language').firstOrNull?.text, 112 | generator: channelElement.findElements('generator').firstOrNull?.text, 113 | copyright: channelElement.findElements('copyright').firstOrNull?.text, 114 | docs: channelElement.findElements('docs').firstOrNull?.text, 115 | managingEditor: 116 | channelElement.findElements('managingEditor').firstOrNull?.text, 117 | rating: channelElement.findElements('rating').firstOrNull?.text, 118 | webMaster: channelElement.findElements('webMaster').firstOrNull?.text, 119 | ttl: int.tryParse( 120 | channelElement.findElements('ttl').firstOrNull?.text ?? '0'), 121 | dc: DublinCore.parse(channelElement), 122 | itunes: Itunes.parse(channelElement), 123 | syndication: Syndication.parse(channelElement), 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/domain/rss_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/iterable.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | class RssImage { 5 | final String? title; 6 | final String? url; 7 | final String? link; 8 | 9 | RssImage({this.title, this.url, this.link}); 10 | 11 | factory RssImage.parse(XmlElement element) { 12 | return RssImage( 13 | title: element.findElements('title').firstOrNull?.text, 14 | url: element.findElements('url').firstOrNull?.text, 15 | link: element.findElements('link').firstOrNull?.text, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/domain/rss_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/domain/dublin_core/dublin_core.dart'; 2 | import 'package:webfeed/domain/itunes/itunes.dart'; 3 | import 'package:webfeed/domain/media/media.dart'; 4 | import 'package:webfeed/domain/rss_category.dart'; 5 | import 'package:webfeed/domain/rss_content.dart'; 6 | import 'package:webfeed/domain/rss_enclosure.dart'; 7 | import 'package:webfeed/domain/rss_source.dart'; 8 | import 'package:webfeed/util/datetime.dart'; 9 | import 'package:webfeed/util/iterable.dart'; 10 | import 'package:xml/xml.dart'; 11 | 12 | class RssItem { 13 | final String? title; 14 | final String? description; 15 | final String? link; 16 | 17 | final List? categories; 18 | final String? guid; 19 | final DateTime? pubDate; 20 | final String? author; 21 | final String? comments; 22 | final RssSource? source; 23 | final RssContent? content; 24 | final Media? media; 25 | final RssEnclosure? enclosure; 26 | final DublinCore? dc; 27 | final Itunes? itunes; 28 | 29 | RssItem({ 30 | this.title, 31 | this.description, 32 | this.link, 33 | this.categories, 34 | this.guid, 35 | this.pubDate, 36 | this.author, 37 | this.comments, 38 | this.source, 39 | this.content, 40 | this.media, 41 | this.enclosure, 42 | this.dc, 43 | this.itunes, 44 | }); 45 | 46 | factory RssItem.parse(XmlElement element) { 47 | return RssItem( 48 | title: element.findElements('title').firstOrNull?.text, 49 | description: element.findElements('description').firstOrNull?.text, 50 | link: element.findElements('link').firstOrNull?.text, 51 | categories: element 52 | .findElements('category') 53 | .map((e) => RssCategory.parse(e)) 54 | .toList(), 55 | guid: element.findElements('guid').firstOrNull?.text, 56 | pubDate: parseDateTime(element.findElements('pubDate').firstOrNull?.text), 57 | author: element.findElements('author').firstOrNull?.text, 58 | comments: element.findElements('comments').firstOrNull?.text, 59 | source: element 60 | .findElements('source') 61 | .map((e) => RssSource.parse(e)) 62 | .firstOrNull, 63 | content: element 64 | .findElements('content:encoded') 65 | .map((e) => RssContent.parse(e)) 66 | .firstOrNull, 67 | media: Media.parse(element), 68 | enclosure: element 69 | .findElements('enclosure') 70 | .map((e) => RssEnclosure.parse(e)) 71 | .firstOrNull, 72 | dc: DublinCore.parse(element), 73 | itunes: Itunes.parse(element), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/domain/rss_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:xml/xml.dart'; 2 | 3 | class RssSource { 4 | final String? url; 5 | final String? value; 6 | 7 | RssSource(this.url, this.value); 8 | 9 | factory RssSource.parse(XmlElement element) { 10 | var url = element.getAttribute('url'); 11 | var value = element.text; 12 | 13 | return RssSource(url, value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/domain/syndication/syndication.dart: -------------------------------------------------------------------------------- 1 | import 'package:webfeed/util/datetime.dart'; 2 | import 'package:webfeed/util/iterable.dart'; 3 | import 'package:xml/xml.dart'; 4 | 5 | enum SyndicationUpdatePeriod { hourly, daily, weekly, monthly, yearly } 6 | 7 | class Syndication { 8 | final SyndicationUpdatePeriod? updatePeriod; 9 | final int? updateFrequency; 10 | final DateTime? updateBase; 11 | 12 | Syndication({ 13 | this.updatePeriod, 14 | this.updateFrequency, 15 | this.updateBase, 16 | }); 17 | 18 | factory Syndication.parse(XmlElement element) { 19 | SyndicationUpdatePeriod updatePeriod; 20 | switch (element.findElements('sy:updatePeriod').firstOrNull?.text) { 21 | case 'hourly': 22 | updatePeriod = SyndicationUpdatePeriod.hourly; 23 | break; 24 | case 'daily': 25 | updatePeriod = SyndicationUpdatePeriod.daily; 26 | break; 27 | case 'weekly': 28 | updatePeriod = SyndicationUpdatePeriod.weekly; 29 | break; 30 | case 'monthly': 31 | updatePeriod = SyndicationUpdatePeriod.monthly; 32 | break; 33 | case 'yearly': 34 | updatePeriod = SyndicationUpdatePeriod.yearly; 35 | break; 36 | default: 37 | updatePeriod = SyndicationUpdatePeriod.daily; 38 | break; 39 | } 40 | return Syndication( 41 | updatePeriod: updatePeriod, 42 | updateFrequency: int.tryParse( 43 | element.findElements('sy:updateFrequency').firstOrNull?.text ?? '1'), 44 | updateBase: parseDateTime( 45 | element.findElements('sy:updateBase').firstOrNull?.text), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/datetime.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | const rfc822DatePattern = 'EEE, dd MMM yyyy HH:mm:ss Z'; 4 | 5 | DateTime? parseDateTime(dateString) { 6 | if (dateString == null) return null; 7 | return _parseRfc822DateTime(dateString) ?? _parseIso8601DateTime(dateString); 8 | } 9 | 10 | DateTime? _parseRfc822DateTime(String dateString) { 11 | try { 12 | final num? length = dateString.length.clamp(0, rfc822DatePattern.length); 13 | final trimmedPattern = rfc822DatePattern.substring(0, length as int?); //Some feeds use a shortened RFC 822 date, e.g. 'Tue, 04 Aug 2020' 14 | final format = DateFormat(trimmedPattern, 'en_US'); 15 | return format.parse(dateString); 16 | } on FormatException { 17 | return null; 18 | } 19 | } 20 | 21 | DateTime? _parseIso8601DateTime(dateString) { 22 | try { 23 | return DateTime.parse(dateString); 24 | } on FormatException { 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/util/function.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/witochandra/webfeed/9f9b6035245dc5a72ada2b1293a2d8adc7cc2420/lib/util/function.dart -------------------------------------------------------------------------------- /lib/util/iterable.dart: -------------------------------------------------------------------------------- 1 | extension WebFeedIterable on Iterable { 2 | T? get firstOrNull => isEmpty ? null : first; 3 | } 4 | -------------------------------------------------------------------------------- /lib/util/xml.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:webfeed/util/iterable.dart'; 4 | import 'package:xml/xml.dart'; 5 | 6 | Iterable? findElements( 7 | XmlNode? node, 8 | String name, { 9 | bool recursive = false, 10 | String? namespace, 11 | }) { 12 | try { 13 | if (recursive) { 14 | return node?.findAllElements(name, namespace: namespace); 15 | } else { 16 | return node?.findElements(name, namespace: namespace); 17 | } 18 | } on StateError { 19 | return null; 20 | } 21 | } 22 | 23 | bool parseBoolLiteral(XmlElement element, String tagName) { 24 | var v = element.findElements(tagName).firstOrNull?.text.toLowerCase().trim(); 25 | if (v == null) return false; 26 | return ['yes', 'true'].contains(v); 27 | } 28 | -------------------------------------------------------------------------------- /lib/webfeed.dart: -------------------------------------------------------------------------------- 1 | export 'domain/atom_category.dart'; 2 | export 'domain/atom_feed.dart'; 3 | export 'domain/atom_generator.dart'; 4 | export 'domain/atom_item.dart'; 5 | export 'domain/atom_link.dart'; 6 | export 'domain/atom_person.dart'; 7 | export 'domain/atom_source.dart'; 8 | export 'domain/rss_category.dart'; 9 | export 'domain/rss_cloud.dart'; 10 | export 'domain/rss_feed.dart'; 11 | export 'domain/rss_image.dart'; 12 | export 'domain/rss_item.dart'; 13 | export 'domain/rss_source.dart'; 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: webfeed 2 | version: 0.7.0 3 | description: webfeed is a dart package for parsing RSS and Atom feeds. Media, DublinCore, iTunes, Syndication namespaces are also supported. 4 | homepage: https://github.com/witochandra/webfeed 5 | environment: 6 | sdk: '>=2.12.0 <3.0.0' 7 | dependencies: 8 | xml: "^5.0.2" 9 | intl: "^0.17.0" 10 | dev_dependencies: 11 | test: ^1.3.0 12 | http: "^0.13.0" 13 | -------------------------------------------------------------------------------- /test/atom_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'dart:io'; 3 | 4 | import 'package:test/test.dart'; 5 | import 'package:webfeed/webfeed.dart'; 6 | 7 | void main() { 8 | test('parse Invalid.xml', () { 9 | var xmlString = File('test/xml/Invalid.xml').readAsStringSync(); 10 | 11 | try { 12 | AtomFeed.parse(xmlString); 13 | fail('Should throw Argument Error'); 14 | } on ArgumentError {} 15 | }); 16 | 17 | test('parse Atom.xml', () { 18 | var xmlString = File('test/xml/Atom.xml').readAsStringSync(); 19 | 20 | var feed = AtomFeed.parse(xmlString); 21 | 22 | expect(feed.id, 'foo-bar-id'); 23 | expect(feed.title, 'Foo bar news'); 24 | expect(feed.updated, DateTime.utc(2018, 4, 6, 13, 2, 46)); 25 | 26 | expect(feed.links!.length, 2); 27 | expect(feed.links!.first.rel, 'foo'); 28 | expect(feed.links!.first.type, 'text/html'); 29 | expect(feed.links!.first.hreflang, 'en'); 30 | expect(feed.links!.first.href, 'http://foo.bar.news/'); 31 | expect(feed.links!.first.title, 'Foo bar news html'); 32 | expect(feed.links!.first.length, 1000); 33 | 34 | expect(feed.authors!.length, 2); 35 | expect(feed.authors!.first.name, 'Alice'); 36 | expect(feed.authors!.first.uri, 'http://foo.bar.news/people/alice'); 37 | expect(feed.authors!.first.email, 'alice@foo.bar.news'); 38 | 39 | expect(feed.contributors!.length, 2); 40 | expect(feed.contributors!.first.name, 'Charlie'); 41 | expect(feed.contributors!.first.uri, 'http://foo.bar.news/people/charlie'); 42 | expect(feed.contributors!.first.email, 'charlie@foo.bar.news'); 43 | 44 | expect(feed.categories!.length, 2); 45 | expect(feed.categories!.first.term, 'foo category'); 46 | expect(feed.categories!.first.scheme, 'this-is-foo-scheme'); 47 | expect(feed.categories!.first.label, 'this is foo label'); 48 | 49 | expect(feed.generator!.uri, 'http://foo.bar.news/generator'); 50 | expect(feed.generator!.version, '1.0'); 51 | expect(feed.generator!.value, 'Foo bar generator'); 52 | 53 | expect(feed.icon, 'http://foo.bar.news/icon.png'); 54 | expect(feed.logo, 'http://foo.bar.news/logo.png'); 55 | expect(feed.subtitle, 'This is subtitle'); 56 | 57 | expect(feed.items!.length, 2); 58 | var item = feed.items!.first; 59 | expect(item.id, 'foo-bar-entry-id-1'); 60 | expect(item.title, 'Foo bar item 1'); 61 | expect(item.updated, DateTime.utc(2018, 4, 6, 13, 2, 40)); 62 | 63 | expect(item.authors!.length, 2); 64 | expect(item.authors!.first.name, 'Ellie'); 65 | expect(item.authors!.first.uri, 'http://foo.bar.news/people/ellie'); 66 | expect(item.authors!.first.email, 'ellie@foo.bar.news'); 67 | 68 | expect(item.links!.length, 2); 69 | expect(item.links!.first.rel, 'foo entry'); 70 | expect(item.links!.first.type, 'text/html'); 71 | expect(item.links!.first.hreflang, 'en'); 72 | expect(item.links!.first.href, 'http://foo.bar.news/entry'); 73 | expect(item.links!.first.title, 'Foo bar news html'); 74 | expect(item.links!.first.length, 1000); 75 | 76 | expect(item.categories!.length, 2); 77 | expect(item.categories!.first.term, 'foo entry category'); 78 | expect(item.categories!.first.scheme, 'this-is-foo-entry-scheme'); 79 | expect(item.categories!.first.label, 'this is foo entry label'); 80 | 81 | expect(item.contributors!.length, 2); 82 | expect(item.contributors!.first.name, 'Gin'); 83 | expect(item.contributors!.first.uri, 'http://foo.bar.news/people/gin'); 84 | expect(item.contributors!.first.email, 'gin@foo.bar.news'); 85 | 86 | expect(item.published, '2018-04-06T13:02:49Z'); 87 | expect(item.summary, 'This is summary 1'); 88 | expect(item.content, 'This is content 1'); 89 | expect(item.rights, 'This is rights 1'); 90 | }); 91 | test('parse Atom-Media.xml', () { 92 | var xmlString = File('test/xml/Atom-Media.xml').readAsStringSync(); 93 | 94 | var feed = AtomFeed.parse(xmlString); 95 | expect(feed.id, 'foo-bar-id'); 96 | expect(feed.title, 'Foo bar news'); 97 | expect(feed.updated, DateTime.utc(2018, 4, 6, 13, 2, 46)); 98 | 99 | expect(feed.items!.length, 1); 100 | 101 | var item = feed.items!.first; 102 | expect(item.media!.group!.contents!.length, 5); 103 | expect(item.media!.group!.credits!.length, 2); 104 | expect(item.media!.group!.category!.value, 'music/artist name/album/song'); 105 | expect(item.media!.group!.rating!.value, 'nonadult'); 106 | 107 | expect(item.media!.contents!.length, 2); 108 | var mediaContent = item.media!.contents!.first; 109 | expect(mediaContent.url, 'http://www.foo.com/video.mov'); 110 | expect(mediaContent.type, 'video/quicktime'); 111 | expect(mediaContent.fileSize, 2000); 112 | expect(mediaContent.medium, 'video'); 113 | expect(mediaContent.isDefault, true); 114 | expect(mediaContent.expression, 'full'); 115 | expect(mediaContent.bitrate, 128); 116 | expect(mediaContent.framerate, 25); 117 | expect(mediaContent.samplingrate, 44.1); 118 | expect(mediaContent.channels, 2); 119 | 120 | expect(item.media!.credits!.length, 2); 121 | var mediaCredit = item.media!.credits!.first; 122 | expect(mediaCredit.role, 'owner1'); 123 | expect(mediaCredit.scheme, 'urn:yvs'); 124 | expect(mediaCredit.value, 'copyright holder of the entity'); 125 | 126 | expect(item.media!.category!.scheme, 127 | 'http://search.yahoo.com/mrss/category_ schema'); 128 | expect(item.media!.category!.label, 'Music'); 129 | expect(item.media!.category!.value, 'music/artist/album/song'); 130 | 131 | expect(item.media!.rating!.scheme, 'urn:simple'); 132 | expect(item.media!.rating!.value, 'adult'); 133 | 134 | expect(item.media!.title!.type, 'plain'); 135 | expect(item.media!.title!.value, "The Judy's -- The Moo Song"); 136 | 137 | expect(item.media!.description!.type, 'plain'); 138 | expect(item.media!.description!.value, 139 | 'This was some really bizarre band I listened to as a young lad.'); 140 | 141 | expect(item.media!.keywords, 'kitty, cat, big dog, yarn, fluffy'); 142 | 143 | expect(item.media!.thumbnails!.length, 2); 144 | var mediaThumbnail = item.media!.thumbnails!.first; 145 | expect(mediaThumbnail.url, 'http://www.foo.com/keyframe1.jpg'); 146 | expect(mediaThumbnail.width, '75'); 147 | expect(mediaThumbnail.height, '50'); 148 | expect(mediaThumbnail.time, '12:05:01.123'); 149 | 150 | expect(item.media!.hash!.algo, 'md5'); 151 | expect(item.media!.hash!.value, 'dfdec888b72151965a34b4b59031290a'); 152 | 153 | expect(item.media!.player!.url, 'http://www.foo.com/player?id=1111'); 154 | expect(item.media!.player!.width, 400); 155 | expect(item.media!.player!.height, 200); 156 | expect(item.media!.player!.value, ''); 157 | 158 | expect(item.media!.copyright!.url, 'http://blah.com/additional-info.html'); 159 | expect(item.media!.copyright!.value, '2005 FooBar Media'); 160 | 161 | expect(item.media!.text!.type, 'plain'); 162 | expect(item.media!.text!.lang, 'en'); 163 | expect(item.media!.text!.start, '00:00:03.000'); 164 | expect(item.media!.text!.end, '00:00:10.000'); 165 | expect(item.media!.text!.value, ' Oh, say, can you see'); 166 | 167 | expect(item.media!.restriction!.relationship, 'allow'); 168 | expect(item.media!.restriction!.type, 'country'); 169 | expect(item.media!.restriction!.value, 'au us'); 170 | 171 | expect(item.media!.community!.starRating!.average, 3.5); 172 | expect(item.media!.community!.starRating!.count, 20); 173 | expect(item.media!.community!.starRating!.min, 1); 174 | expect(item.media!.community!.starRating!.max, 10); 175 | expect(item.media!.community!.statistics!.views, 5); 176 | expect(item.media!.community!.statistics!.favorites, 4); 177 | expect(item.media!.community!.tags!.tags, 'news: 5, abc:3'); 178 | expect(item.media!.community!.tags!.weight, 1); 179 | 180 | expect(item.media!.comments!.length, 2); 181 | expect(item.media!.comments!.first, 'comment1'); 182 | expect(item.media!.comments!.last, 'comment2'); 183 | 184 | expect(item.media!.embed!.url, 'http://www.foo.com/player.swf'); 185 | expect(item.media!.embed!.width, 512); 186 | expect(item.media!.embed!.height, 323); 187 | expect(item.media!.embed!.params!.length, 5); 188 | expect(item.media!.embed!.params!.first.name, 'type'); 189 | expect(item.media!.embed!.params!.first.value, 190 | 'application/x-shockwave-flash'); 191 | 192 | expect(item.media!.responses!.length, 2); 193 | expect(item.media!.responses!.first, 'http://www.response1.com'); 194 | expect(item.media!.responses!.last, 'http://www.response2.com'); 195 | 196 | expect(item.media!.backLinks!.length, 2); 197 | expect(item.media!.backLinks!.first, 'http://www.backlink1.com'); 198 | expect(item.media!.backLinks!.last, 'http://www.backlink2.com'); 199 | 200 | expect(item.media!.status!.state, 'active'); 201 | expect(item.media!.status!.reason, null); 202 | 203 | expect(item.media!.prices!.length, 2); 204 | expect(item.media!.prices!.first.price, 19.99); 205 | expect(item.media!.prices!.first.type, 'rent'); 206 | expect(item.media!.prices!.first.info, 207 | 'http://www.dummy.jp/package_info.html'); 208 | expect(item.media!.prices!.first.currency, 'EUR'); 209 | 210 | expect(item.media!.license!.type, 'text/html'); 211 | expect(item.media!.license!.href, 'http://www.licensehost.com/license'); 212 | expect(item.media!.license!.value, ' Sample license for a video'); 213 | 214 | expect(item.media!.peerLink!.type, 'application/x-bittorrent'); 215 | expect(item.media!.peerLink!.href, 'http://www.foo.org/sampleFile.torrent'); 216 | expect(item.media!.peerLink!.value, ''); 217 | 218 | expect(item.media!.rights!.status, 'official'); 219 | 220 | expect(item.media!.scenes!.length, 2); 221 | expect(item.media!.scenes!.first.title, 'sceneTitle1'); 222 | expect(item.media!.scenes!.first.description, 'sceneDesc1'); 223 | expect(item.media!.scenes!.first.startTime, '00:15'); 224 | expect(item.media!.scenes!.first.endTime, '00:45'); 225 | }); 226 | 227 | test('parse Atom-Empty.xml', () { 228 | var xmlString = File('test/xml/Atom-Empty.xml').readAsStringSync(); 229 | 230 | var feed = AtomFeed.parse(xmlString); 231 | 232 | expect(feed.id, null); 233 | expect(feed.title, null); 234 | expect(feed.updated, null); 235 | expect(feed.links!.length, 0); 236 | expect(feed.authors!.length, 0); 237 | expect(feed.contributors!.length, 0); 238 | expect(feed.categories!.length, 0); 239 | expect(feed.generator, null); 240 | expect(feed.icon, null); 241 | expect(feed.logo, null); 242 | expect(feed.subtitle, null); 243 | 244 | expect(feed.items!.length, 1); 245 | var item = feed.items!.first; 246 | 247 | expect(item.authors!.length, 0); 248 | 249 | expect(item.links!.length, 0); 250 | 251 | expect(item.categories!.length, 0); 252 | 253 | expect(item.contributors!.length, 0); 254 | 255 | expect(item.published, null); 256 | expect(item.summary, null); 257 | expect(item.content, null); 258 | expect(item.rights, null); 259 | }); 260 | } 261 | -------------------------------------------------------------------------------- /test/rss_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'dart:io'; 3 | 4 | import 'package:test/test.dart'; 5 | import 'package:webfeed/domain/itunes/itunes_episode_type.dart'; 6 | import 'package:webfeed/domain/itunes/itunes_type.dart'; 7 | import 'package:webfeed/domain/syndication/syndication.dart'; 8 | import 'package:webfeed/webfeed.dart'; 9 | 10 | void main() { 11 | test('parse Invalid.xml', () { 12 | var xmlString = File('test/xml/Invalid.xml').readAsStringSync(); 13 | 14 | try { 15 | RssFeed.parse(xmlString); 16 | fail('Should throw Argument Error'); 17 | } on ArgumentError {} 18 | }); 19 | test('parse RSS.xml', () { 20 | var xmlString = File('test/xml/RSS.xml').readAsStringSync(); 21 | 22 | var feed = RssFeed.parse(xmlString); 23 | 24 | expect(feed.title, 'News - Foo bar News'); 25 | expect(feed.description, 26 | 'Foo bar News and Updates feed provided by Foo bar, Inc.'); 27 | expect(feed.link, 'https://foo.bar.news/'); 28 | expect(feed.author, 'hello@world.net'); 29 | expect(feed.language, 'en-US'); 30 | expect(feed.lastBuildDate, 'Mon, 26 Mar 2018 14:00:00 PDT'); 31 | expect(feed.generator, 'Custom'); 32 | expect(feed.copyright, 'Copyright 2018, Foo bar Inc.'); 33 | expect(feed.docs, 'https://foo.bar.news/docs'); 34 | expect(feed.managingEditor, 'alice@foo.bar.news'); 35 | expect(feed.rating, 'The PICS rating of the feed'); 36 | expect(feed.webMaster, 'webmaster@foo.bar.news'); 37 | expect(feed.ttl, 60); 38 | 39 | expect(feed.image!.title, 'Foo bar News'); 40 | expect(feed.image!.url, 'https://foo.bar.news/logo.gif'); 41 | expect(feed.image!.link, 'https://foo.bar.news/'); 42 | 43 | expect(feed.cloud!.domain, 'radio.foo.bar.news'); 44 | expect(feed.cloud!.port, '80'); 45 | expect(feed.cloud!.path, '/RPC2'); 46 | expect(feed.cloud!.registerProcedure, 'foo.bar.rssPleaseNotify'); 47 | expect(feed.cloud!.protocol, 'xml-rpc'); 48 | 49 | expect(feed.categories!.length, 2); 50 | expect(feed.categories![0].domain, null); 51 | expect(feed.categories![0].value, 'Ipsum'); 52 | expect(feed.categories![1].domain, 'news'); 53 | expect(feed.categories![1].value, 'Lorem Ipsum'); 54 | 55 | expect(feed.skipDays!.length, 3); 56 | expect(feed.skipDays!.contains('Monday'), true); 57 | expect(feed.skipDays!.contains('Tuesday'), true); 58 | expect(feed.skipDays!.contains('Sunday'), true); 59 | 60 | expect(feed.skipHours!.length, 5); 61 | expect(feed.skipHours!.contains(0), true); 62 | expect(feed.skipHours!.contains(1), true); 63 | expect(feed.skipHours!.contains(2), true); 64 | expect(feed.skipHours!.contains(3), true); 65 | expect(feed.skipHours!.contains(4), true); 66 | 67 | expect(feed.items!.length, 2); 68 | 69 | expect(feed.items!.first.title, 70 | 'The standard Lorem Ipsum passage, used since the 1500s'); 71 | expect(feed.items!.first.description, 72 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'); 73 | expect(feed.items!.first.link, 'https://foo.bar.news/1'); 74 | expect(feed.items!.first.guid, 'https://foo.bar.news/1?guid'); 75 | expect(feed.items!.first.pubDate, 76 | DateTime(2018, 03, 26, 14)); //Mon, 26 Mar 2018 14:00:00 PDT 77 | expect(feed.items!.first.categories!.first.domain, 'news'); 78 | expect(feed.items!.first.categories!.first.value, 'Lorem'); 79 | expect(feed.items!.first.author, 'alice@foo.bar.news'); 80 | expect(feed.items!.first.source!.url, 'https://foo.bar.news/1?source'); 81 | expect(feed.items!.first.source!.value, 'Foo Bar'); 82 | expect(feed.items!.first.comments, 'https://foo.bar.news/1/comments'); 83 | expect(feed.items!.first.enclosure!.url, 84 | 'http://www.scripting.com/mp3s/weatherReportSuite.mp3'); 85 | expect(feed.items!.first.enclosure!.length, 12216320); 86 | expect(feed.items!.first.enclosure!.type, 'audio/mpeg'); 87 | 88 | expect(feed.items!.first.content!.value, 89 | ' Test content
'); 90 | expect( 91 | feed.items!.first.content!.images.first, 'https://test.com/image_link'); 92 | }); 93 | test('parse RSS-Media.xml', () { 94 | var xmlString = File('test/xml/RSS-Media.xml').readAsStringSync(); 95 | 96 | var feed = RssFeed.parse(xmlString); 97 | expect(feed.title, 'Song Site'); 98 | expect( 99 | feed.description, 'Media RSS example with new fields added in v1.5.0'); 100 | 101 | expect(feed.items!.length, 1); 102 | 103 | var item = feed.items!.first; 104 | expect(item.title, null); 105 | expect(item.link, 'http://www.foo.com'); 106 | expect(item.pubDate, 107 | DateTime(2001, 08, 27, 16, 08, 56)); //Mon, 27 Aug 2001 16:08:56 PST 108 | 109 | expect(item.media!.group!.contents!.length, 5); 110 | expect(item.media!.group!.credits!.length, 2); 111 | expect(item.media!.group!.category!.value, 'music/artist name/album/song'); 112 | expect(item.media!.group!.rating!.value, 'nonadult'); 113 | 114 | expect(item.media!.contents!.length, 2); 115 | var mediaContent = item.media!.contents!.first; 116 | expect(mediaContent.url, 'http://www.foo.com/video.mov'); 117 | expect(mediaContent.type, 'video/quicktime'); 118 | expect(mediaContent.fileSize, 2000); 119 | expect(mediaContent.medium, 'video'); 120 | expect(mediaContent.isDefault, true); 121 | expect(mediaContent.expression, 'full'); 122 | expect(mediaContent.bitrate, 128); 123 | expect(mediaContent.framerate, 25); 124 | expect(mediaContent.samplingrate, 44.1); 125 | expect(mediaContent.channels, 2); 126 | 127 | expect(item.media!.credits!.length, 2); 128 | var mediaCredit = item.media!.credits!.first; 129 | expect(mediaCredit.role, 'owner1'); 130 | expect(mediaCredit.scheme, 'urn:yvs'); 131 | expect(mediaCredit.value, 'copyright holder of the entity'); 132 | 133 | expect(item.media!.category!.scheme, 134 | 'http://search.yahoo.com/mrss/category_ schema'); 135 | expect(item.media!.category!.label, 'Music'); 136 | expect(item.media!.category!.value, 'music/artist/album/song'); 137 | 138 | expect(item.media!.rating!.scheme, 'urn:simple'); 139 | expect(item.media!.rating!.value, 'adult'); 140 | 141 | expect(item.media!.title!.type, 'plain'); 142 | expect(item.media!.title!.value, "The Judy's -- The Moo Song"); 143 | 144 | expect(item.media!.description!.type, 'plain'); 145 | expect(item.media!.description!.value, 146 | 'This was some really bizarre band I listened to as a young lad.'); 147 | 148 | expect(item.media!.keywords, 'kitty, cat, big dog, yarn, fluffy'); 149 | 150 | expect(item.media!.thumbnails!.length, 2); 151 | var mediaThumbnail = item.media!.thumbnails!.first; 152 | expect(mediaThumbnail.url, 'http://www.foo.com/keyframe1.jpg'); 153 | expect(mediaThumbnail.width, '75'); 154 | expect(mediaThumbnail.height, '50'); 155 | expect(mediaThumbnail.time, '12:05:01.123'); 156 | 157 | expect(item.media!.hash!.algo, 'md5'); 158 | expect(item.media!.hash!.value, 'dfdec888b72151965a34b4b59031290a'); 159 | 160 | expect(item.media!.player!.url, 'http://www.foo.com/player?id=1111'); 161 | expect(item.media!.player!.width, 400); 162 | expect(item.media!.player!.height, 200); 163 | expect(item.media!.player!.value, ''); 164 | 165 | expect(item.media!.copyright!.url, 'http://blah.com/additional-info.html'); 166 | expect(item.media!.copyright!.value, '2005 FooBar Media'); 167 | 168 | expect(item.media!.text!.type, 'plain'); 169 | expect(item.media!.text!.lang, 'en'); 170 | expect(item.media!.text!.start, '00:00:03.000'); 171 | expect(item.media!.text!.end, '00:00:10.000'); 172 | expect(item.media!.text!.value, ' Oh, say, can you see'); 173 | 174 | expect(item.media!.restriction!.relationship, 'allow'); 175 | expect(item.media!.restriction!.type, 'country'); 176 | expect(item.media!.restriction!.value, 'au us'); 177 | 178 | expect(item.media!.community!.starRating!.average, 3.5); 179 | expect(item.media!.community!.starRating!.count, 20); 180 | expect(item.media!.community!.starRating!.min, 1); 181 | expect(item.media!.community!.starRating!.max, 10); 182 | expect(item.media!.community!.statistics!.views, 5); 183 | expect(item.media!.community!.statistics!.favorites, 4); 184 | expect(item.media!.community!.tags!.tags, 'news: 5, abc:3'); 185 | expect(item.media!.community!.tags!.weight, 1); 186 | 187 | expect(item.media!.comments!.length, 2); 188 | expect(item.media!.comments!.first, 'comment1'); 189 | expect(item.media!.comments!.last, 'comment2'); 190 | 191 | expect(item.media!.embed!.url, 'http://www.foo.com/player.swf'); 192 | expect(item.media!.embed!.width, 512); 193 | expect(item.media!.embed!.height, 323); 194 | expect(item.media!.embed!.params!.length, 5); 195 | expect(item.media!.embed!.params!.first.name, 'type'); 196 | expect(item.media!.embed!.params!.first.value, 197 | 'application/x-shockwave-flash'); 198 | 199 | expect(item.media!.responses!.length, 2); 200 | expect(item.media!.responses!.first, 'http://www.response1.com'); 201 | expect(item.media!.responses!.last, 'http://www.response2.com'); 202 | 203 | expect(item.media!.backLinks!.length, 2); 204 | expect(item.media!.backLinks!.first, 'http://www.backlink1.com'); 205 | expect(item.media!.backLinks!.last, 'http://www.backlink2.com'); 206 | 207 | expect(item.media!.status!.state, 'active'); 208 | expect(item.media!.status!.reason, null); 209 | 210 | expect(item.media!.prices!.length, 2); 211 | expect(item.media!.prices!.first.price, 19.99); 212 | expect(item.media!.prices!.first.type, 'rent'); 213 | expect(item.media!.prices!.first.info, 214 | 'http://www.dummy.jp/package_info.html'); 215 | expect(item.media!.prices!.first.currency, 'EUR'); 216 | 217 | expect(item.media!.license!.type, 'text/html'); 218 | expect(item.media!.license!.href, 'http://www.licensehost.com/license'); 219 | expect(item.media!.license!.value, ' Sample license for a video'); 220 | 221 | expect(item.media!.peerLink!.type, 'application/x-bittorrent'); 222 | expect(item.media!.peerLink!.href, 'http://www.foo.org/sampleFile.torrent'); 223 | expect(item.media!.peerLink!.value, ''); 224 | 225 | expect(item.media!.rights!.status, 'official'); 226 | 227 | expect(item.media!.scenes!.length, 2); 228 | expect(item.media!.scenes!.first.title, 'sceneTitle1'); 229 | expect(item.media!.scenes!.first.description, 'sceneDesc1'); 230 | expect(item.media!.scenes!.first.startTime, '00:15'); 231 | expect(item.media!.scenes!.first.endTime, '00:45'); 232 | }); 233 | test('parse RSS-DC.xml', () { 234 | var xmlString = File('test/xml/RSS-DC.xml').readAsStringSync(); 235 | 236 | var feed = RssFeed.parse(xmlString); 237 | 238 | expect(feed.dc!.title, 'title'); 239 | expect(feed.dc!.creator, 'creator'); 240 | expect(feed.dc!.subject, 'subject'); 241 | expect(feed.dc!.description, 'description'); 242 | expect(feed.dc!.publisher, 'publisher'); 243 | expect(feed.dc!.contributor, 'contributor'); 244 | expect(feed.dc!.date, DateTime.utc(2000, 1, 1, 12)); 245 | expect(feed.dc!.created, DateTime.utc(2000, 1, 1, 13)); 246 | expect(feed.dc!.modified, DateTime.utc(2000, 1, 1, 14)); 247 | expect(feed.dc!.type, 'type'); 248 | expect(feed.dc!.format, 'format'); 249 | expect(feed.dc!.identifier, 'identifier'); 250 | expect(feed.dc!.source, 'source'); 251 | expect(feed.dc!.language, 'language'); 252 | expect(feed.dc!.relation, 'relation'); 253 | expect(feed.dc!.coverage, 'coverage'); 254 | expect(feed.dc!.rights, 'rights'); 255 | 256 | expect(feed.items!.first.dc!.title, 'title'); 257 | expect(feed.items!.first.dc!.creator, 'creator'); 258 | expect(feed.items!.first.dc!.subject, 'subject'); 259 | expect(feed.items!.first.dc!.description, 'description'); 260 | expect(feed.items!.first.dc!.publisher, 'publisher'); 261 | expect(feed.items!.first.dc!.contributor, 'contributor'); 262 | expect(feed.items!.first.dc!.date, DateTime.utc(2000, 1, 2, 12)); 263 | expect(feed.items!.first.dc!.created, DateTime.utc(2000, 1, 2, 13)); 264 | expect(feed.items!.first.dc!.modified, DateTime.utc(2000, 1, 2, 14)); 265 | expect(feed.items!.first.dc!.type, 'type'); 266 | expect(feed.items!.first.dc!.format, 'format'); 267 | expect(feed.items!.first.dc!.identifier, 'identifier'); 268 | expect(feed.items!.first.dc!.source, 'source'); 269 | expect(feed.items!.first.dc!.language, 'language'); 270 | expect(feed.items!.first.dc!.relation, 'relation'); 271 | expect(feed.items!.first.dc!.coverage, 'coverage'); 272 | expect(feed.items!.first.dc!.rights, 'rights'); 273 | }); 274 | 275 | test('parse RSS-Empty.xml', () { 276 | var xmlString = File('test/xml/RSS-Empty.xml').readAsStringSync(); 277 | 278 | var feed = RssFeed.parse(xmlString); 279 | 280 | expect(feed.title, null); 281 | expect(feed.description, null); 282 | expect(feed.link, null); 283 | expect(feed.language, null); 284 | expect(feed.lastBuildDate, null); 285 | expect(feed.generator, null); 286 | expect(feed.copyright, null); 287 | expect(feed.docs, null); 288 | expect(feed.managingEditor, null); 289 | expect(feed.rating, null); 290 | expect(feed.webMaster, null); 291 | expect(feed.ttl, 0); 292 | 293 | expect(feed.image, null); 294 | 295 | expect(feed.cloud, null); 296 | 297 | expect(feed.categories!.length, 0); 298 | 299 | expect(feed.skipDays!.length, 0); 300 | 301 | expect(feed.skipHours!.length, 0); 302 | 303 | expect(feed.items!.length, 1); 304 | 305 | expect(feed.items!.first.title, null); 306 | expect(feed.items!.first.description, null); 307 | expect(feed.items!.first.link, null); 308 | expect(feed.items!.first.guid, null); 309 | expect(feed.items!.first.pubDate, null); 310 | expect(feed.items!.first.categories!.length, 0); 311 | expect(feed.items!.first.author, null); 312 | expect(feed.items!.first.source, null); 313 | expect(feed.items!.first.comments, null); 314 | expect(feed.items!.first.enclosure, null); 315 | 316 | expect(feed.items!.first.content, null); 317 | }); 318 | 319 | test('parse RSS-Itunes.xml', () { 320 | var xmlString = File('test/xml/RSS-Itunes.xml').readAsStringSync(); 321 | 322 | var feed = RssFeed.parse(xmlString); 323 | 324 | expect(feed.itunes!.author, 'Changelog Media'); 325 | expect(feed.itunes!.summary, 'Foo'); 326 | expect(feed.itunes!.explicit, false); 327 | expect(feed.itunes!.image!.href, 328 | 'https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357'); 329 | expect(feed.itunes!.keywords, 330 | 'go,golang,open source,software,development'.split(',')); 331 | expect(feed.itunes!.owner!.name, 'Changelog Media'); 332 | expect(feed.itunes!.owner!.email, 'editors@changelog.com'); 333 | expect( 334 | Set.from([ 335 | feed.itunes!.categories![0].category, 336 | feed.itunes!.categories![1].category 337 | ]), 338 | ['Technology', 'Foo']); 339 | for (var category in feed.itunes!.categories!) { 340 | switch (category.category) { 341 | case 'Foo': 342 | expect(category.subCategories, ['Bar', 'Baz']); 343 | break; 344 | case 'Technology': 345 | expect(category.subCategories, ['Software How-To', 'Tech News']); 346 | break; 347 | } 348 | } 349 | expect(feed.itunes!.title, 'Go Time'); 350 | expect(feed.itunes!.type, ItunesType.serial); 351 | expect(feed.itunes!.newFeedUrl, 'wubawuba'); 352 | expect(feed.itunes!.block, true); 353 | expect(feed.itunes!.complete, true); 354 | 355 | var item = feed.items![0]; 356 | expect(item.itunes!.episodeType, ItunesEpisodeType.full); 357 | expect(item.itunes!.episode, 1); 358 | expect(item.itunes!.season, 1); 359 | expect(item.itunes!.image!.href, 360 | 'https://cdn.changelog.com/uploads/covers/go-time-original.png?v=63725770357'); 361 | expect(item.itunes!.duration, Duration(minutes: 32, seconds: 30)); 362 | expect(item.itunes!.explicit, false); 363 | expect(item.itunes!.keywords, 364 | 'go,golang,open source,software,development'.split(',')); 365 | expect(item.itunes!.subtitle, 'with Erik, Carlisia, and Brian'); 366 | expect(item.itunes!.summary, 'Foo'); 367 | expect(item.itunes!.author, 368 | 'Erik St. Martin, Carlisia Pinto, and Brian Ketelsen'); 369 | expect(item.itunes!.explicit, false); 370 | expect(item.itunes!.title, 'awesome title'); 371 | expect(item.itunes!.block, false); 372 | }); 373 | 374 | test('parse RSS-Itunes_item_empty_field.xml with empty duration field', () { 375 | var xmlString = 376 | File('test/xml/RSS-Itunes_item_empty_field.xml').readAsStringSync(); 377 | 378 | var feed = RssFeed.parse(xmlString); 379 | 380 | expect(feed.itunes?.owner?.name, 'Changelog Media'); 381 | var item = feed.items![0]; 382 | expect(item.itunes?.episodeType, ItunesEpisodeType.full); 383 | expect(item.itunes?.duration, null); 384 | expect(item.itunes?.title, 'awesome title'); 385 | }); 386 | 387 | test('parse RSS-RDF.xml', () { 388 | var xmlString = File('test/xml/RSS-RDF.xml').readAsStringSync(); 389 | 390 | var feed = RssFeed.parse(xmlString); 391 | 392 | expect(feed.title, 'Mozilla Dot Org'); 393 | expect(feed.link, 'http://www.mozilla.org'); 394 | expect(feed.description, 'the Mozilla Organization web site'); 395 | expect(feed.image!.title, 'Mozilla'); 396 | expect(feed.image!.url, 'http://www.mozilla.org/images/moz.gif'); 397 | expect(feed.image!.link, 'http://www.mozilla.org'); 398 | expect(feed.items!.length, 5); 399 | expect(feed.items!.first.title, 'New Status Updates'); 400 | expect(feed.items!.first.link, 'http://www.mozilla.org/status/'); 401 | }); 402 | 403 | test('parse RSS-Syndication.xml', () { 404 | var xmlString = File('test/xml/RSS-Syndication.xml').readAsStringSync(); 405 | 406 | var feed = RssFeed.parse(xmlString); 407 | 408 | expect(feed.title, 'Meerkat'); 409 | expect(feed.link, 'http://meerkat.oreillynet.com'); 410 | expect(feed.description, 'Meerkat: An Open Wire Service'); 411 | expect(feed.image!.title, 'Meerkat Powered!'); 412 | expect(feed.image!.url, 413 | 'http://meerkat.oreillynet.com/icons/meerkat-powered.jpg'); 414 | expect(feed.image!.link, 'http://meerkat.oreillynet.com'); 415 | expect(feed.syndication!.updatePeriod, SyndicationUpdatePeriod.hourly); 416 | expect(feed.syndication!.updateFrequency, 2); 417 | expect(feed.syndication!.updateBase, DateTime.utc(2001, 1, 1, 12, 1)); 418 | expect(feed.items!.length, 1); 419 | expect(feed.items!.first.title, 'XML: A Disruptive Technology'); 420 | expect(feed.items!.first.description, 421 | 'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.'); 422 | expect(feed.items!.first.link, 'http://c.moreover.com/click/here.pl?r123'); 423 | }); 424 | } 425 | -------------------------------------------------------------------------------- /test/xml/Atom-Empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/xml/Atom-Media.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | foo-bar-id 4 | Foo bar news 5 | 2018-04-06T13:02:46Z 6 | 7 | 8 | 9 | Alice 10 | http://foo.bar.news/people/alice 11 | alice@foo.bar.news 12 | 13 | 14 | Bob 15 | http://foo.bar.news/people/bob 16 | bob@foo.bar.news 17 | 18 | 19 | Charlie 20 | http://foo.bar.news/people/charlie 21 | charlie@foo.bar.news 22 | 23 | 24 | David 25 | http://foo.bar.news/people/david 26 | david@foo.bar.news 27 | 28 | 29 | 30 | Foo bar generator 31 | http://foo.bar.news/icon.png 32 | http://foo.bar.news/logo.png 33 | This is subtitle 34 | 35 | foo-bar-entry-id-1 36 | Foo bar item 1 37 | 38 | 39 | copyright holder of the entity 40 | copyright holder of the entity 41 | music/artist/album/song 42 | adult 43 | 44 | 45 | 46 | 47 | 48 | 49 | band member 1 50 | band member 2 51 | music/artist name/album/song 52 | nonadult 53 | 54 | The Judy's -- The Moo Song 55 | This was some really bizarre band I listened to as a young lad. 56 | kitty, cat, big dog, yarn, fluffy 57 | 58 | 59 | dfdec888b72151965a34b4b59031290a 60 | 61 | 2005 FooBar Media 62 | Oh, say, can you see 63 | au us 64 | 65 | 66 | 67 | news: 5, abc:3 68 | 69 | 70 | comment1 71 | comment2 72 | 73 | 74 | application/x-shockwave-flash 75 | 512 76 | 323 77 | true 78 | id=12345&vid=678912i&lang=en-us&intl=us&thumbUrl=http://www.foo.com/thumbnail.jpg 79 | 80 | 81 | 82 | http://www.response1.com 83 | http://www.response2.com 84 | 85 | 86 | http://www.backlink1.com 87 | http://www.backlink2.com 88 | 89 | 90 | 91 | 92 | Sample license for a video 93 | 94 | 95 | 96 | 97 | 98 | 35.669998 139.770004 99 | 100 | 101 | 102 | 103 | 104 | 105 | sceneTitle1 106 | sceneDesc1 107 | 00:15 108 | 00:45 109 | 110 | 111 | sceneTitle2 112 | sceneDesc2 113 | 00:15 114 | 00:55 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /test/xml/Atom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | foo-bar-id 4 | Foo bar news 5 | 2018-04-06T13:02:46Z 6 | 7 | 9 | 10 | Alice 11 | http://foo.bar.news/people/alice 12 | alice@foo.bar.news 13 | 14 | 15 | Bob 16 | http://foo.bar.news/people/bob 17 | bob@foo.bar.news 18 | 19 | 20 | Charlie 21 | http://foo.bar.news/people/charlie 22 | charlie@foo.bar.news 23 | 24 | 25 | David 26 | http://foo.bar.news/people/david 27 | david@foo.bar.news 28 | 29 | 30 | 31 | Foo bar generator 32 | http://foo.bar.news/icon.png 33 | http://foo.bar.news/logo.png 34 | This is subtitle 35 | 36 | foo-bar-entry-id-1 37 | Foo bar item 1 38 | 2018-04-06T13:02:40Z 39 | 40 | Ellie 41 | http://foo.bar.news/people/ellie 42 | ellie@foo.bar.news 43 | 44 | 45 | Franz 46 | http://foo.bar.news/people/franz 47 | franz@foo.bar.news 48 | 49 | 51 | 53 | 54 | 55 | 56 | Gin 57 | http://foo.bar.news/people/gin 58 | gin@foo.bar.news 59 | 60 | 61 | Hanz 62 | http://foo.bar.news/people/hanz 63 | hanz@foo.bar.news 64 | 65 | 66 | http://foo.bar.news/source 67 | Foo bar source 68 | 2018-04-06T13:02:48Z 69 | 70 | 71 | 2018-04-06T13:02:49Z 72 | This is summary 1 73 | This is content 1 74 | This is rights 1 75 | 76 | 77 | foo-bar-entry-id-2 78 | Foo bar item 2 79 | 2018-04-06T13:02:50Z 80 | 81 | Iris 82 | http://foo.bar.news/people/iris 83 | iris@foo.bar.news 84 | 85 | 86 | Jhon 87 | http://foo.bar.news/people/jhon 88 | jhon@foo.bar.news 89 | 90 | 92 | 94 | 95 | 96 | 97 | Kevin 98 | http://foo.bar.news/people/kevin 99 | kevin@foo.bar.news 100 | 101 | 102 | Lucy 103 | http://foo.bar.news/people/lucy 104 | lucy@foo.bar.news 105 | 106 | 107 | http://foo.bar.news/source 108 | Foo bar source 109 | 2018-04-06T13:02:51Z 110 | 111 | 112 | 2018-04-06T13:02:52Z 113 | This is summary 2 114 | This is content 2 115 | This is rights 2 116 | 117 | -------------------------------------------------------------------------------- /test/xml/Invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/xml/RSS-DC.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | title 6 | creator 7 | subject 8 | description 9 | publisher 10 | contributor 11 | 2000-01-01T12:00+00:00 12 | 2000-01-01T13:00+00:00 13 | 2000-01-01T14:00+00:00 14 | type 15 | format 16 | identifier 17 | source 18 | language 19 | relation 20 | coverage 21 | rights 22 | 23 | title 24 | creator 25 | subject 26 | description 27 | publisher 28 | contributor 29 | 2000-01-02T12:00+00:00 30 | 2000-01-02T13:00+00:00 31 | 2000-01-02T14:00+00:00 32 | type 33 | format 34 | identifier 35 | source 36 | language 37 | relation 38 | coverage 39 | rights 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/xml/RSS-Empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/xml/RSS-Itunes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Go Time 4 | Go Time 5 | serial 6 | Yes 7 | TRUE 8 | wubawuba 9 | All rights reserved 10 | https://changelog.com/gotime 11 | 12 | 13 | 14 | 15 | en-us 16 | 17 | 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! 18 | 19 | Changelog Media 20 | Foo 21 | no 22 | 23 | go, golang, open source, software, development 24 | 25 | Changelog Media 26 | editors@changelog.com 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | awesome title 38 | full 39 | 1 40 | 1 41 | xxx 42 | 43 | 32:30 44 | no 45 | go, golang, open source, software, development 46 | with Erik, Carlisia, and Brian 47 | Foo 48 | Erik St. Martin, Carlisia Pinto, and Brian Ketelsen 49 | 50 | Erik St. Martin, Carlisia Pinto, and Brian Ketelsen 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/xml/RSS-Itunes_item_empty_field.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Go Time 4 | Go Time 5 | serial 6 | Yes 7 | TRUE 8 | wubawuba 9 | All rights reserved 10 | https://changelog.com/gotime 11 | 12 | 13 | 14 | 15 | en-us 16 | 17 | 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! 18 | 19 | Changelog Media 20 | Foo 21 | no 22 | 23 | go, golang, open source, software, development 24 | 25 | Changelog Media 26 | editors@changelog.com 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | awesome title 38 | full 39 | 1 40 | 1 41 | xxx 42 | 43 | 44 | no 45 | go, golang, open source, software, development 46 | with Erik, Carlisia, and Brian 47 | Foo 48 | Erik St. Martin, Carlisia Pinto, and Brian Ketelsen 49 | 50 | Erik St. Martin, Carlisia Pinto, and Brian Ketelsen 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/xml/RSS-Media.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | Song Site 8 | Media RSS example with new fields added in v1.5.0 9 | 10 | http://www.foo.com 11 | Mon, 27 Aug 2001 16:08:56 PST 12 | 13 | 14 | copyright holder of the entity 15 | copyright holder of the entity 16 | music/artist/album/song 17 | adult 18 | 19 | 20 | 21 | 22 | 23 | 24 | band member 1 25 | band member 2 26 | music/artist name/album/song 27 | nonadult 28 | 29 | The Judy's -- The Moo Song 30 | This was some really bizarre band I listened to as a young lad. 31 | kitty, cat, big dog, yarn, fluffy 32 | 33 | 34 | dfdec888b72151965a34b4b59031290a 35 | 36 | 2005 FooBar Media 37 | Oh, say, can you see 38 | au us 39 | 40 | 41 | 42 | news: 5, abc:3 43 | 44 | 45 | comment1 46 | comment2 47 | 48 | 49 | application/x-shockwave-flash 50 | 512 51 | 323 52 | true 53 | id=12345&vid=678912i&lang=en-us&intl=us&thumbUrl=http://www.foo.com/thumbnail.jpg 54 | 55 | 56 | 57 | http://www.response1.com 58 | http://www.response2.com 59 | 60 | 61 | http://www.backlink1.com 62 | http://www.backlink2.com 63 | 64 | 65 | 66 | 67 | Sample license for a video 68 | 69 | 70 | 71 | 72 | 73 | 35.669998 139.770004 74 | 75 | 76 | 77 | 78 | 79 | 80 | sceneTitle1 81 | sceneDesc1 82 | 00:15 83 | 00:45 84 | 85 | 86 | sceneTitle2 87 | sceneDesc2 88 | 00:15 89 | 00:55 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /test/xml/RSS-RDF.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Mozilla Dot Org 7 | http://www.mozilla.org 8 | the Mozilla Organization web site 9 | 10 | 11 | Mozilla 12 | http://www.mozilla.org/images/moz.gif 13 | http://www.mozilla.org 14 | 15 | 16 | New Status Updates 17 | http://www.mozilla.org/status/ 18 | 19 | 20 | Bugzilla Reorganized 21 | http://www.mozilla.org/bugs/ 22 | 23 | 24 | Mozilla Party, 2.0! 25 | http://www.mozilla.org/party/1999/ 26 | 27 | 28 | Unix Platform Parity 29 | http://www.mozilla.org/build/unix.html 30 | 31 | 32 | NPL 1.0M published 33 | http://www.mozilla.org/NPL/NPL-1.0M.html 34 | 35 | -------------------------------------------------------------------------------- /test/xml/RSS-Syndication.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Meerkat 7 | http://meerkat.oreillynet.com 8 | Meerkat: An Open Wire Service 9 | hourly 10 | 2 11 | 2001-01-01T12:01+00:00 12 | 13 | 14 | 15 | Meerkat Powered! 16 | http://meerkat.oreillynet.com/icons/meerkat-powered.jpg 17 | http://meerkat.oreillynet.com 18 | 19 | 20 | XML: A Disruptive Technology 21 | http://c.moreover.com/click/here.pl?r123 22 | XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet. 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/xml/RSS.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | News - Foo bar News 7 | https://foo.bar.news/ 8 | hello@world.net 9 | Foo bar News and Updates feed provided by Foo bar, Inc. 10 | 11 | https://foo.bar.news/logo.gif 12 | Foo bar News 13 | https://foo.bar.news/ 14 | 15 | 17 | Ipsum 18 | Lorem Ipsum 19 | en-US 20 | Mon, 26 Mar 2018 14:00:00 PDT 21 | Custom 22 | Copyright 2018, Foo bar Inc. 23 | https://foo.bar.news/docs 24 | alice@foo.bar.news 25 | The PICS rating of the feed 26 | webmaster@foo.bar.news 27 | 60 28 | 29 | Monday 30 | Tuesday 31 | Sunday 32 | 33 | 34 | 0 35 | 1 36 | 2 37 | 3 38 | 4 39 | 40 | 41 | The standard Lorem Ipsum passage, used since the 1500s 42 | https://foo.bar.news/1 43 | https://foo.bar.news/1?guid 44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit 45 | Lorem 46 | Mon, 26 Mar 2018 14:00:00 PDT 47 | alice@foo.bar.news 48 | Foo Bar 49 | https://foo.bar.news/1/comments 50 | Test content
]]>
51 | 52 |
53 | 54 | Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC 55 | https://foo.bar.news/2 56 | https://foo.bar.news/2?guid 57 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque 58 | laudantium 59 | 60 | Ipsum 61 | Tue, 20 Mar 2018 10:00:00 PDT 62 | bob@foo.bar.news 63 | Lorem Ipsum 64 | https://foo.bar.news/2/comments 65 | 66 |
67 |
--------------------------------------------------------------------------------