1 /++ 2 RSS/Atom feed reading 3 4 References: 5 $(LIST 6 * https://cyber.harvard.edu/rss/rss.html 7 * http://www.rssboard.org/rss-specification 8 * https://tools.ietf.org/html/rfc4287 9 * https://en.wikipedia.org/wiki/Atom_(Web_standard) 10 ) 11 +/ 12 module arsd.rss; 13 14 import arsd.dom; 15 16 /// generic subset of rss and atom, normalized for easy consumption 17 struct Feed { 18 string title; /// 19 string description; /// 20 string lastUpdated; /// 21 22 /// 23 static struct Item { 24 string title; /// 25 string link; /// 26 string description; /// could be html or text! 27 string author; /// Typical format: email (name) 28 string publicationDate; /// the format is 2005-07-31T12:29:29Z 29 string lastUpdatedDate; /// the format is 2005-07-31T12:29:29Z 30 string guid; /// 31 32 string enclosureUri; /// 33 string enclosureType; /// a mime type 34 string enclosureSize; /// 35 } 36 37 Item[] items; /// 38 } 39 40 /+ 41 import arsd.cgi; 42 mixin GenericMain!handler; 43 void handler(Cgi cgi) { 44 cgi.setResponseContentType("application/atom+xml"); 45 cgi.write(feedToAtom(parseFeed(Document.fromUrl("http://dpldocs.info/this-week-in-d/twid.rss", true).root)).toString); 46 } 47 +/ 48 49 /++ 50 Turns a generic feed back into an Atom document. 51 52 History: 53 Added March 18, 2021 54 +/ 55 XmlDocument feedToAtom(Feed feed) { 56 auto document = new XmlDocument(`<feed xmlns="http://www.w3.org/2005/Atom"></feed>`); 57 document.root.addChild("title", feed.title); 58 document.root.addChild("subtitle", feed.description); 59 document.root.addChild("updated", feed.lastUpdated); 60 61 foreach(item; feed.items) { 62 auto entry = document.root.addChild("entry"); 63 entry.addChild("title", item.title); 64 entry.addChild("link").setAttribute("href", item.link); 65 if(item.enclosureUri.length) 66 entry.addChild("link"). 67 setAttribute("rel", "enclosure"). 68 setAttribute("href", item.enclosureUri). 69 setAttribute("length", item.enclosureSize). 70 setAttribute("type", item.enclosureType); 71 entry.addChild("id", item.guid); 72 entry.addChild("published", item.publicationDate); 73 entry.addChild("updated", item.lastUpdatedDate); 74 entry.addChild("content", item.description).setAttribute("type", "html"); // or summary? idk 75 if(item.author.length) { 76 auto author = entry.addChild("author"); 77 import std.string; 78 auto idx = item.author.indexOf("("); 79 if(idx == -1) { 80 author.addChild("email", item.author); 81 } else { 82 if(item.author.length > idx + 2) 83 author.addChild("name", item.author[idx + 1 .. $-1]); 84 author.addChild("email", item.author[0 .. idx -1]); 85 } 86 } 87 } 88 89 return document; 90 } 91 92 /// 93 enum FeedType { 94 unknown, /// 95 rss, /// 96 atom /// 97 } 98 99 /// 100 FeedType identifyFeed(Element e) { 101 assert(e !is null); 102 103 if(e.tagName == "rss") 104 return FeedType.rss; 105 if(e.tagName == "feed" || e.tagName == "atom:feed") 106 return FeedType.atom; 107 108 return FeedType.unknown; 109 } 110 111 /// Parses a feed generically 112 Feed parseFeed(Element e) { 113 final switch(identifyFeed(e)) { 114 case FeedType.unknown: 115 throw new Exception("Unknown feed type"); 116 case FeedType.rss: 117 return parseRss(e).toGenericFeed(); 118 case FeedType.atom: 119 return parseAtom(e).toGenericFeed(); 120 } 121 } 122 123 // application/rss+xml 124 // though some use text/rss+xml or application/rdf+xml 125 126 // root node of <rss version="whatever"> 127 128 struct RssChannel { 129 string title; 130 string link; 131 string description; 132 string lastBuildDate; // last time content in here changed 133 string pubDate; // format like "Sat, 07 Sep 2002 00:00:01 GMT" when it officially changes 134 string docs; // idk? 135 136 string cloud; // has domain, port, path, registerProcedure, protocol 137 138 string language; // optional 139 string copyright; 140 string managingEditor; 141 string webMaster; 142 143 string category; 144 145 string ttl; // in minutes, if present 146 147 RssImage image; 148 149 RssItem[] items; 150 151 Feed toGenericFeed() { 152 Feed f; 153 f.title = this.title; 154 f.description = this.description; // FIXME text vs html? 155 f.lastUpdated = this.lastBuildDate.rssDateToAtom; 156 157 foreach(item; items) { 158 Feed.Item fi; 159 160 fi.title = item.title; 161 fi.link = item.link; 162 fi.description = item.description; // FIXME: try to normalize text vs html 163 fi.author = item.author; // FIXME 164 fi.lastUpdatedDate = fi.publicationDate = item.pubDate.rssDateToAtom; 165 fi.guid = item.guid; 166 //fi.lastUpdatedDate; // not available i think 167 168 fi.enclosureUri = item.enclosure.url; 169 fi.enclosureType = item.enclosure.type; 170 fi.enclosureSize = item.enclosure.length; 171 172 f.items ~= fi; 173 } 174 return f; 175 } 176 } 177 178 struct RssImage { 179 string title; /// img alt 180 string url; /// like the img src 181 string link; /// like a href 182 string width; 183 string height; 184 string description; /// img title 185 } 186 187 struct RssItem { 188 string title; 189 string link; 190 string description; // may have html! 191 192 string author; 193 string category; 194 string comments; // a link 195 196 string pubDate; 197 string guid; 198 199 RssSource source; 200 RssEnclosure enclosure; 201 } 202 203 struct RssEnclosure { 204 string url; 205 string length; 206 string type; 207 } 208 209 struct RssSource { 210 string title; 211 string url; 212 } 213 214 215 /++ 216 Parses RSS into structs. Requires the element to be RSS; if you are unsure 217 of the type and want a generic response, use parseFeed instead. 218 +/ 219 RssChannel parseRss(Element element) { 220 assert(element !is null && element.tagName == "rss"); 221 RssChannel c; 222 element = element.requireSelector(" > channel"); 223 foreach(memberName; __traits(allMembers, RssChannel)) { 224 static if(memberName == "image") { 225 if(auto image = element.querySelector(" > image")) { 226 RssImage i; 227 foreach(mn; __traits(allMembers, RssImage)) { 228 __traits(getMember, i, mn) = image.optionSelector(" > " ~ mn).innerText; 229 } 230 c.image = i; 231 } 232 } else static if(memberName == "items") { 233 foreach(item; element.querySelectorAll(" > item")) { 234 RssItem i; 235 foreach(mn; __traits(allMembers, RssItem)) { 236 static if(mn == "source") { 237 if(auto s = item.querySelector(" > source")) { 238 i.source.title = s.innerText; 239 i.source.url = s.attrs.url; 240 } 241 } else static if(mn == "enclosure") { 242 if(auto s = item.querySelector(" > enclosure")) { 243 i.enclosure.url = s.attrs.url; 244 i.enclosure.type = s.attrs.type; 245 i.enclosure.length = s.attrs.length; 246 } 247 } else { 248 __traits(getMember, i, mn) = item.optionSelector(" > " ~ mn).innerText; 249 } 250 } 251 c.items ~= i; 252 } 253 } else static if(is(typeof( __traits(getMember, c, memberName).offsetof))) { 254 __traits(getMember, c, memberName) = element.optionSelector(" > " ~ memberName).innerText; 255 } 256 } 257 258 return c; 259 } 260 261 /// 262 RssChannel parseRss(string s) { 263 auto document = new Document(s, true, true); 264 return parseRss(document.root); 265 } 266 267 /* 268 struct SyndicationInfo { 269 string updatePeriod; // sy:updatePeriod 270 string updateFrequency; 271 string updateBase; 272 273 string skipHours; // stored as <hour> elements 274 string skipDays; // stored as <day> elements 275 } 276 */ 277 278 279 // /////////////////// atom //////////////////// 280 281 // application/atom+xml 282 283 /+ rss vs atom 284 date format is different 285 atom:xxx links 286 287 root node is <feed>, organization has no <channel>, and <entry> 288 instead of <item> 289 +/ 290 291 /++ 292 293 +/ 294 struct AtomFeed { 295 string title; /// has a type attribute - text or html 296 string subtitle; /// has a type attribute 297 298 string updated; /// io string 299 300 string id; /// 301 string link; /// i want the text/html type really, certainly not rel=self 302 string rights; /// 303 string generator; /// 304 305 AtomEntry[] entries; /// 306 307 /// 308 Feed toGenericFeed() { 309 Feed feed; 310 311 feed.title = this.title; 312 feed.description = this.subtitle; 313 feed.lastUpdated = this.updated; 314 315 foreach(entry; this.entries) { 316 Feed.Item item; 317 318 item.title = entry.title; 319 item.link = entry.link; 320 if(entry.content.html.length || entry.content.text.length) 321 item.description = entry.content.html.length ? entry.content.html : entry.content.text; // FIXME 322 else 323 item.description = entry.summary.html.length ? entry.summary.html : entry.summary.text; // FIXME 324 item.author = entry.author.email; 325 if(entry.author.name.length) 326 item.author ~= " (" ~ entry.author.name ~ ")"; 327 item.publicationDate = entry.published; 328 item.lastUpdatedDate = entry.updated; 329 item.guid = entry.id; 330 331 item.enclosureUri = entry.enclosure.url; 332 item.enclosureType = entry.enclosure.type; 333 item.enclosureSize = entry.enclosure.length; 334 335 feed.items ~= item; 336 } 337 338 return feed; 339 } 340 } 341 342 /// 343 struct AtomEntry { 344 string title; /// 345 string link; /// the alternate 346 AtomEnclosure enclosure; /// 347 string id; /// 348 string updated; /// 349 string published; /// 350 351 AtomPerson author; /// 352 AtomPerson[] contributors; /// 353 AtomContent content; /// // should check type. may also have a src element for a link. type of html is escaped, type of xhtml is embedded. 354 AtomContent summary; /// 355 } 356 357 /// 358 struct AtomEnclosure { 359 string url; /// 360 string length; /// 361 string type; /// 362 } 363 364 365 /// 366 struct AtomContent { 367 string text; /// 368 string html; /// 369 } 370 371 /// 372 struct AtomPerson { 373 string name; /// 374 string uri; /// 375 string email; /// 376 } 377 378 /// 379 AtomFeed parseAtom(Element ele) { 380 AtomFeed af; 381 af.title = ele.optionSelector(` > title, > atom\:title`).innerText; 382 af.subtitle = ele.optionSelector(` > subtitle, > atom\:subtitle`).innerText; 383 af.id = ele.optionSelector(` > id, > atom\:id`).innerText; 384 af.updated = ele.optionSelector(` > updated, > atom\:updated`).innerText; 385 af.rights = ele.optionSelector(` > rights, > atom\:rights`).innerText; 386 af.generator = ele.optionSelector(` > generator, > atom\:generator`).innerText; 387 af.link = ele.optionSelector(` > link:not([rel])`).getAttribute("href"); 388 389 foreach(entry; ele.querySelectorAll(` > entry`)) { 390 AtomEntry ae; 391 392 ae.title = entry.optionSelector(` > title, > atom\:title`).innerText; 393 ae.updated = entry.optionSelector(` > updated, > atom\:updated`).innerText; 394 ae.published = entry.optionSelector(` > published, > atom\:published`).innerText; 395 ae.id = entry.optionSelector(` > id, > atom\:id`).innerText; 396 397 ae.link = entry.optionSelector(` > link:not([rel]), > link[rel=alternate], > link[type="type/html"]`).getAttribute("href"); 398 399 if(auto enclosure = entry.querySelector(` > link[rel=enclosure]`)) { 400 ae.enclosure.url = enclosure.attrs.href; 401 ae.enclosure.length = enclosure.attrs.length; 402 ae.enclosure.type = enclosure.attrs.type; 403 } 404 405 if(auto author = entry.querySelector(` > author`)) { 406 ae.author.name = author.optionSelector(` > name`).innerText; 407 ae.author.uri = author.optionSelector(` > uri`).innerText; 408 ae.author.email = author.optionSelector(` > email`).innerText; 409 } 410 411 foreach(contributor; entry.querySelectorAll(` > contributor`)) { 412 AtomPerson c; 413 c.name = contributor.optionSelector(` > name`).innerText; 414 c.uri = contributor.optionSelector(` > uri`).innerText; 415 c.email = contributor.optionSelector(` > email`).innerText; 416 ae.contributors ~= c; 417 } 418 419 if(auto e = entry.querySelector("content[type=xhtml]")) 420 ae.content.html = e.innerHTML; 421 if(auto e = entry.querySelector("content[type=html]")) 422 ae.content.html = e.innerText; 423 if(auto e = entry.querySelector("content[type=text], content:not([type])")) 424 ae.content.text = e.innerText; 425 426 if(auto e = entry.querySelector("summary[type=xhtml]")) 427 ae.summary.html = e.innerHTML; 428 if(auto e = entry.querySelector("summary[type=html]")) 429 ae.summary.html = e.innerText; 430 if(auto e = entry.querySelector("summary[type=text], summary:not([type])")) 431 ae.summary.text = e.innerText; 432 433 af.entries ~= ae; 434 } 435 436 return af; 437 } 438 439 AtomFeed parseAtom(string s) { 440 auto document = new Document(s, true, true); 441 return parseAtom(document.root); 442 } 443 444 string rssDateToAtom(string d) { 445 auto orig = d; 446 if(d.length < 22 || d[3] != ',') 447 return orig; // doesn't appear to be the right format 448 d = d[5 .. $]; 449 450 import std.conv; 451 auto day = parse!int(d); 452 if(d.length == 0 || d[0] != ' ') 453 return orig; 454 d = d[1 .. $]; 455 456 if(d.length < 4) 457 return orig; 458 459 int month; 460 461 string months = "JanFebMarAprMayJunJulAugSepOctNovDec"; 462 foreach(i; 0 .. 12) { 463 if(months[i * 3 .. i * 3 + 3] == d[0 .. 3]) { 464 month = i + 1; 465 break; 466 } 467 } 468 469 d = d[4 .. $]; 470 471 auto year = parse!int(d); 472 473 if(d.length == 0 || d[0] != ' ') 474 return orig; 475 d = d[1 .. $]; 476 477 auto hour = parse!int(d); 478 479 if(d.length == 0 || d[0] != ':') 480 return orig; 481 d = d[1 .. $]; 482 483 auto minute = parse!int(d); 484 485 if(d.length == 0 || d[0] != ':') 486 return orig; 487 d = d[1 .. $]; 488 489 auto second = parse!int(d); 490 491 import std.format; 492 return format("%04d-%02d-%02dT%02d:%02d:%02dZ", year, month, day, hour, minute, second); 493 } 494 unittest { 495 assert(rssDateToAtom("Mon, 18 Nov 2019 12:05:44 GMT") == "2019-11-18T12:05:44Z"); 496 } 497 498 unittest { 499 500 auto test1 = `<?xml version="1.0" encoding="ISO-8859-1"?> 501 <rss version="0.91"> 502 <channel> 503 <title>WriteTheWeb</title> 504 <link>http://writetheweb.com</link> 505 <description>News for web users that write back</description> 506 <language>en-us</language> 507 <copyright>Copyright 2000, WriteTheWeb team.</copyright> 508 <managingEditor>editor@writetheweb.com</managingEditor> 509 <webMaster>webmaster@writetheweb.com</webMaster> 510 <image> 511 <title>WriteTheWeb</title> 512 <url>http://writetheweb.com/images/mynetscape88.gif</url> 513 <link>http://writetheweb.com</link> 514 <width>88</width> 515 <height>31</height> 516 <description>News for web users that write back</description> 517 </image> 518 <item> 519 <title>Giving the world a pluggable Gnutella</title> 520 <link>http://writetheweb.com/read.php?item=24</link> 521 <description>WorldOS is a framework on which to build programs that work like Freenet or Gnutella -allowing distributed applications using peer-to-peer routing.</description> 522 </item> 523 <item> 524 <title>Syndication discussions hot up</title> 525 <link>http://writetheweb.com/read.php?item=23</link> 526 <description>After a period of dormancy, the Syndication mailing list has become active again, with contributions from leaders in traditional media and Web syndication.</description> 527 </item> 528 <item> 529 <title>Personal web server integrates file sharing and messaging</title> 530 <link>http://writetheweb.com/read.php?item=22</link> 531 <description>The Magi Project is an innovative project to create a combined personal web server and messaging system that enables the sharing and synchronization of information across desktop, laptop and palmtop devices.</description> 532 </item> 533 <item> 534 <title>Syndication and Metadata</title> 535 <link>http://writetheweb.com/read.php?item=21</link> 536 <description>RSS is probably the best known metadata format around. RDF is probably one of the least understood. In this essay, published on my O'Reilly Network weblog, I argue that the next generation of RSS should be based on RDF.</description> 537 </item> 538 <item> 539 <title>UK bloggers get organised</title> 540 <link>http://writetheweb.com/read.php?item=20</link> 541 <description>Looks like the weblogs scene is gathering pace beyond the shores of the US. There's now a UK-specific page on weblogs.com, and a mailing list at egroups.</description> 542 </item> 543 <item> 544 <title>Yournamehere.com more important than anything</title> 545 <link>http://writetheweb.com/read.php?item=19</link> 546 <description>Whatever you're publishing on the web, your site name is the most valuable asset you have, according to Carl Steadman.</description> 547 </item> 548 </channel> 549 </rss>`; 550 551 552 { 553 auto e = parseRss(test1); 554 assert(e.items.length == 6); 555 assert(e.items[$-1].title == "Yournamehere.com more important than anything", e.items[$-1].title); 556 assert(e.items[0].title == "Giving the world a pluggable Gnutella"); 557 assert(e.items[0].link == "http://writetheweb.com/read.php?item=24"); 558 assert(e.image.url == "http://writetheweb.com/images/mynetscape88.gif"); 559 560 auto df = e.toGenericFeed(); 561 assert(df.items.length == 6); 562 assert(df.items[0].link == "http://writetheweb.com/read.php?item=24"); 563 } 564 565 auto test2 = `<?xml version="1.0"?> 566 <!-- RSS generation done by 'Radio UserLand' on Fri, 13 Apr 2001 19:23:02 GMT --> 567 <rss version="0.92"> 568 <channel> 569 <title>Dave Winer: Grateful Dead</title> 570 <link>http://www.scripting.com/blog/categories/gratefulDead.html</link> 571 <description>A high-fidelity Grateful Dead song every day. This is where we're experimenting with enclosures on RSS news items that download when you're not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. </description> 572 <lastBuildDate>Fri, 13 Apr 2001 19:23:02 GMT</lastBuildDate> 573 <docs>http://backend.userland.com/rss092</docs> 574 <managingEditor>dave@userland.com (Dave Winer)</managingEditor> 575 <webMaster>dave@userland.com (Dave Winer)</webMaster> 576 <cloud domain="data.ourfavoritesongs.com" port="80" path="/RPC2" registerProcedure="ourFavoriteSongs.rssPleaseNotify" protocol="xml-rpc"/> 577 <item> 578 <description>It's been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it's #16 on the hotlist of upstreaming Radio users, there's no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight's song is a live version of Weather Report Suite from Dick's Picks Volume 7. It's wistful music. Of course a beautiful song, oft-quoted here on Scripting News. <i>A little change, the wind and rain.</i> 579 </description> 580 <enclosure url="http://www.scripting.com/mp3s/weatherReportDicksPicsVol7.mp3" length="6182912" type="audio/mpeg"/> 581 </item> 582 <item> 583 <description>Kevin Drennan started a <a href="http://deadend.editthispage.com/">Grateful Dead Weblog</a>. Hey it's cool, he even has a <a href="http://deadend.editthispage.com/directory/61">directory</a>. <i>A Frontier 7 feature.</i></description> 584 <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source> 585 </item> 586 <item> 587 <description><a href="http://arts.ucsc.edu/GDead/AGDL/other1.html">The Other One</a>, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time.</description> 588 <enclosure url="http://www.scripting.com/mp3s/theOtherOne.mp3" length="6666097" type="audio/mpeg"/> 589 </item> 590 <item> 591 <description>This is a test of a change I just made. Still diggin..</description> 592 </item> 593 <item> 594 <description>The HTML rendering almost <a href="http://validator.w3.org/check/referer">validates</a>. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don't know. If you're blind send me an email if u cn rd ths. </description> 595 </item> 596 <item> 597 <description><a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin's_Tower.txt">Franklin's Tower</a>, a live version from One From The Vault.</description> 598 <enclosure url="http://www.scripting.com/mp3s/franklinsTower.mp3" length="6701402" type="audio/mpeg"/> 599 </item> 600 <item> 601 <description>Moshe Weitzman says Shakedown Street is what I'm lookin for for tonight. I'm listening right now. It's one of my favorites. "Don't tell me this town ain't got no heart." Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? "Spanish lady come to me.."</description> 602 <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source> 603 </item> 604 <item> 605 <description><a href="http://www.scripting.com/mp3s/youWinAgain.mp3">The news is out</a>, all over town..<p> 606 You've been seen, out runnin round. <p> 607 The lyrics are <a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt">here</a>, short and sweet. <p> 608 <i>You win again!</i> 609 </description> 610 <enclosure url="http://www.scripting.com/mp3s/youWinAgain.mp3" length="3874816" type="audio/mpeg"/> 611 </item> 612 <item> 613 <description><a href="http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm">Weather Report Suite</a>: "Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain."</description> 614 <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" length="12216320" type="audio/mpeg"/> 615 </item> 616 <item> 617 <description><a href="http://arts.ucsc.edu/gdead/agdl/darkstar.html">Dark Star</a> crashes, pouring its light into ashes.</description> 618 <enclosure url="http://www.scripting.com/mp3s/darkStar.mp3" length="10889216" type="audio/mpeg"/> 619 </item> 620 <item> 621 <description>DaveNet: <a href="http://davenet.userland.com/2001/01/21/theUsBlues">The U.S. Blues</a>.</description> 622 </item> 623 <item> 624 <description>Still listening to the US Blues. <i>"Wave that flag, wave it wide and high.."</i> Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let's take it back. Hey I'm still a hippie. <i>"You could call this song The United States Blues."</i></description> 625 </item> 626 <item> 627 <description><a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i></description> 628 <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/> 629 </item> 630 <item> 631 <description>Grateful Dead: "Tennessee, Tennessee, ain't no place I'd rather be."</description> 632 <enclosure url="http://www.scripting.com/mp3s/tennesseeJed.mp3" length="3442648" type="audio/mpeg"/> 633 </item> 634 <item> 635 <description>Ed Cone: "Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band's finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha."</description> 636 <enclosure url="http://www.scripting.com/mp3s/darkStarWharfRat.mp3" length="27503386" type="audio/mpeg"/> 637 </item> 638 <item> 639 <description><a href="http://arts.ucsc.edu/GDead/AGDL/fotd.html">Tonight's Song</a>: "If I get home before daylight I just might get some sleep tonight." </description> 640 <enclosure url="http://www.scripting.com/mp3s/friendOfTheDevil.mp3" length="3219742" type="audio/mpeg"/> 641 </item> 642 <item> 643 <description><a href="http://arts.ucsc.edu/GDead/AGDL/uncle.html">Tonight's song</a>: "Come hear Uncle John's Band by the river side. Got some things to talk about here beside the rising tide."</description> 644 <enclosure url="http://www.scripting.com/mp3s/uncleJohnsBand.mp3" length="4587102" type="audio/mpeg"/> 645 </item> 646 <item> 647 <description><a href="http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt">Me and My Uncle</a>: "I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road." 648 </description> 649 <enclosure url="http://www.scripting.com/mp3s/meAndMyUncle.mp3" length="2949248" type="audio/mpeg"/> 650 </item> 651 <item> 652 <description>Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain't worth a dime, if you don't lay em down.</description> 653 <enclosure url="http://www.scripting.com/mp3s/truckin.mp3" length="4847908" type="audio/mpeg"/> 654 </item> 655 <item> 656 <description>Two-Way-Web: <a href="http://www.thetwowayweb.com/payloadsForRss">Payloads for RSS</a>. "When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn't want to hear about it."</description> 657 </item> 658 <item> 659 <description>A touch of gray, kinda suits you anyway..</description> 660 <enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/> 661 </item> 662 <item> 663 <description><a href="http://www.sixties.com/html/garcia_stack_0.html"><img src="http://www.scripting.com/images/captainTripsSmall.gif" height="51" width="42" border="0" hspace="10" vspace="10" align="right"></a>In celebration of today's inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the <a href="http://searchlyrics2.homestead.com/gd_usblues.html">lyrics</a>. Click on the audio icon to the left to give it a listen. "Red and white, blue suede shoes, I'm Uncle Sam, how do you do?" It's a different kind of patriotic music, but man I love my country and I love Jerry and the band. <i>I truly do!</i></description> 664 <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/> 665 </item> 666 </channel> 667 </rss><?xml version="1.0"?>`; 668 669 { 670 auto e = parseRss(test2); 671 assert(e.items[$-1].enclosure.url == "http://www.scripting.com/mp3s/usBlues.mp3"); 672 } 673 674 auto test3 = `<rss version="2.0"> 675 <channel> 676 <title>Liftoff News</title> 677 <link>http://liftoff.msfc.nasa.gov/</link> 678 <description>Liftoff to Space Exploration.</description> 679 <language>en-us</language> 680 <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate> 681 <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate> 682 <docs>http://blogs.law.harvard.edu/tech/rss</docs> 683 <generator>Weblog Editor 2.0</generator> 684 <managingEditor>editor@example.com</managingEditor> 685 <webMaster>webmaster@example.com</webMaster> 686 <item> 687 <title>Star City</title> 688 <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link> 689 <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description> 690 <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate> 691 <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid> 692 </item> 693 <item> 694 <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st.</description> 695 <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate> 696 <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid> 697 </item> 698 <item> 699 <title>The Engine That Does More</title> 700 <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link> 701 <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description> 702 <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate> 703 <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid> 704 </item> 705 <item> 706 <title>Astronauts' Dirty Laundry</title> 707 <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link> 708 <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description> 709 <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate> 710 <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid> 711 </item> 712 </channel> 713 </rss>`; 714 715 716 auto testAtom1 = `<?xml version="1.0" encoding="utf-8"?> 717 718 <feed xmlns="http://www.w3.org/2005/Atom"> 719 720 <title>Example Feed</title> 721 <subtitle>A subtitle.</subtitle> 722 <link href="http://example.org/feed/" rel="self" /> 723 <link href="http://example.org/" /> 724 <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id> 725 <updated>2003-12-13T18:30:02Z</updated> 726 727 728 <entry> 729 <title>Atom-Powered Robots Run Amok</title> 730 <link href="http://example.org/2003/12/13/atom03" /> 731 <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/> 732 <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/> 733 <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> 734 <updated>2003-12-13T18:30:02Z</updated> 735 <summary>Some text.</summary> 736 <content type="xhtml"> 737 <div xmlns="http://www.w3.org/1999/xhtml"> 738 <p>This is the entry content.</p> 739 </div> 740 </content> 741 <author> 742 <name>John Doe</name> 743 <email>johndoe@example.com</email> 744 </author> 745 </entry> 746 747 </feed>`; 748 749 { 750 auto e = parseAtom(testAtom1); 751 752 assert(e.entries.length == 1); 753 assert(e.link == "http://example.org/"); 754 assert(e.title == "Example Feed"); 755 assert(e.entries[0].title == "Atom-Powered Robots Run Amok"); 756 assert(e.entries[0].link == "http://example.org/2003/12/13/atom03", e.entries[0].link); 757 assert(e.entries[0].summary.text == "Some text.", e.entries[0].summary.text); 758 assert(e.entries[0].summary.html.length == 0); 759 assert(e.entries[0].content.text.length == 0); 760 assert(e.entries[0].content.html.length > 10); 761 762 auto gf = e.toGenericFeed(); 763 764 assert(gf.items[0].lastUpdatedDate == "2003-12-13T18:30:02Z"); 765 } 766 767 { 768 auto xml = `<rss version="2.0"> 769 <channel> 770 <title>NYT > World News</title> 771 <link> 772 https://www.nytimes.com/section/world?emc=rss&partner=rss 773 </link> 774 <atom:link href="https://rss.nytimes.com/services/xml/rss/nyt/World.xml" rel="self" type="application/rss+xml"/> 775 <description/> 776 <language>en-us</language> 777 <copyright>Copyright 2019 The New York Times Company</copyright> 778 <lastBuildDate>Sat, 07 Dec 2019 00:15:41 +0000</lastBuildDate> 779 <image> 780 <title>NYT > World News</title> 781 <url> 782 https://static01.nyt.com/images/misc/NYT_logo_rss_250x40.png 783 </url> 784 <link> 785 https://www.nytimes.com/section/world?emc=rss&partner=rss 786 </link> 787 </image> 788 <item> 789 <title> 790 France Is Hit by Second Day of Pension Strikes as Unions Dig In 791 </title> 792 <link>https://www.nytimes.com/2019/12/06/world/europe/france-pension-strike-macron.html?emc=rss&partner=rss</link> 793 <guid isPermaLink="true"> 794 https://www.nytimes.com/2019/12/06/world/europe/france-pension-strike-macron.html 795 </guid> 796 <atom:link href="https://www.nytimes.com/2019/12/06/world/europe/france-pension-strike-macron.html?emc=rss&partner=rss" rel="standout"/> 797 <description> 798 Transportation was severely disrupted in Paris and other cities, a day after huge protests over government plans to overhaul pensions. Unions are planning more protests next week. 799 </description> 800 <dc:creator>Aurelien Breeden</dc:creator> 801 <pubDate>Fri, 06 Dec 2019 18:02:13 +0000</pubDate> 802 <category domain="http://www.nytimes.com/namespaces/keywords/nyt_geo">France</category> 803 <category domain="http://www.nytimes.com/namespaces/keywords/des">Demonstrations, Protests and Riots</category> 804 <category domain="http://www.nytimes.com/namespaces/keywords/des">Pensions and Retirement Plans</category> 805 <category domain="http://www.nytimes.com/namespaces/keywords/des">Politics and Government</category> 806 <category domain="http://www.nytimes.com/namespaces/keywords/des">Strikes</category> 807 <category domain="http://www.nytimes.com/namespaces/keywords/nyt_per">Macron, Emmanuel (1977- )</category> 808 <media:content height="151" medium="image" url="https://static01.nyt.com/images/2019/12/06/world/06france-strikes/merlin_165509820_476d5340-3717-4fbb-b187-097ae7718e48-moth.jpg" width="151"/> 809 <media:credit>Rafael Yaghobzadeh/Associated Press</media:credit> 810 <media:description> 811 A deserted Gare de Lyon train station in Paris on Friday. Unions are aiming for a protracted strike. 812 </media:description> 813 </item></channel></rss>`; 814 815 auto e = parseRss(xml); 816 assert(e.items[0].link == "https://www.nytimes.com/2019/12/06/world/europe/france-pension-strike-macron.html?emc=rss&partner=rss", e.items[0].link); 817 818 auto gf = e.toGenericFeed(); 819 assert(gf.items[0].link == "https://www.nytimes.com/2019/12/06/world/europe/france-pension-strike-macron.html?emc=rss&partner=rss", e.items[0].link); 820 821 assert(gf.items[0].publicationDate == "2019-12-06T18:02:13Z"); 822 } 823 824 }