From 89cfff32971ab95250ca0bbe1a2c57e3170ad368 Mon Sep 17 00:00:00 2001
From: n451 <2020200706@ruc.edu.cn>
Date: Sun, 23 Feb 2025 18:38:59 +0800
Subject: [PATCH] fix(parser): missing author field fix(parser): author is
optional at parsing, fallback at rendering feat(date): support weeks_ago
test(parser): add tests from sample-feeds.com
---
data/sample/apple_podcast.xml | 198 +++++++
data/sample/atom.xml | 19 +
data/sample/feedforall.xml | 151 +++++
data/sample/youtube.xml | 974 +++++++++++++++++++++++++++++++
data/url_atom.xml | 24 +-
data/url_atom2.xml | 24 +-
lua/feed/commands.lua | 2 +-
lua/feed/config.lua | 2 +-
lua/feed/fetch.lua | 16 +-
lua/feed/health.lua | 1 +
lua/feed/integrations/rsshub.lua | 6 +-
lua/feed/opml.lua | 54 +-
lua/feed/parser/atom.lua | 64 +-
lua/feed/parser/date.lua | 118 ++--
lua/feed/parser/init.lua | 18 +-
lua/feed/parser/jsonfeed.lua | 36 +-
lua/feed/parser/rss.lua | 42 +-
lua/feed/parser/utils.lua | 30 -
lua/feed/parser/xml.lua | 4 +-
lua/feed/ui.lua | 11 +-
lua/feed/ui/format.lua | 20 +-
lua/feed/ui/progress.lua | 5 +
lua/feed/utils.lua | 7 +
lua/feed/utils/shared.lua | 27 +
lua/feed/utils/url.lua | 6 +-
tests/helpers.lua | 9 +-
tests/test_date.lua | 18 +-
tests/test_db.lua | 6 +-
tests/test_feedparser.lua | 116 +++-
tests/test_opml.lua | 1 +
30 files changed, 1751 insertions(+), 258 deletions(-)
create mode 100644 data/sample/apple_podcast.xml
create mode 100644 data/sample/atom.xml
create mode 100644 data/sample/feedforall.xml
create mode 100644 data/sample/youtube.xml
delete mode 100644 lua/feed/parser/utils.lua
diff --git a/data/sample/apple_podcast.xml b/data/sample/apple_podcast.xml
new file mode 100644
index 0000000..b51615d
--- /dev/null
+++ b/data/sample/apple_podcast.xml
@@ -0,0 +1,198 @@
+
+
+
+ Hiking Treks
+ https://www.apple.com/itunes/podcasts/
+ en-us
+ © 2020 John Appleseed
+ The Sunset Explorers
+
+ Love to get outdoors and discover nature's treasures? Hiking Treks is the
+ show for you. We review hikes and excursions, review outdoor gear and interview
+ a variety of naturalists and adventurers. Look for new episodes each week.
+
+ serial
+
+ Sunset Explorers
+ mountainscape@icloud.com
+
+
+
+
+
+ false
+ -
+ trailer
+ Hiking Treks Trailer
+
+ Apple Podcasts.]]>
+
+
+ aae20190418
+ Tue, 8 Jan 2019 01:15:00 GMT
+ 1079
+ false
+
+ -
+ full
+ 4
+ 2
+ S02 EP04 Mt. Hood, Oregon
+
+ Tips for trekking around the tallest mountain in Oregon
+
+
+ aae20190606
+ Tue, 07 May 2019 12:00:00 GMT
+ 1024
+ false
+
+ -
+ full
+ 3
+ 2
+ S02 EP03 Bouldering Around Boulder
+
+ We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.
+
+
+ href="http://example.com/podcasts/everything/
+
+ aae20190530
+ Tue, 30 Apr 2019 13:00:00 EST
+ 3627
+ false
+
+ -
+ full
+ 2
+ 2
+ S02 EP02 Caribou Mountain, Maine
+
+ Put your fitness to the test with this invigorating hill climb.
+
+
+
+ aae20190523
+ Tue, 23 May 2019 02:00:00 -0700
+ 2434
+ false
+
+ -
+ full
+ 1
+ 2
+ S02 EP01 Stawamus Chief
+
+ We tackle Stawamus Chief outside of Vancouver, BC and you should too!
+
+
+ aae20190516
+ 2019-02-16T07:00:00.000Z
+ 13:24
+ false
+
+ -
+ full
+ 4
+ 1
+ S01 EP04 Kuliouou Ridge Trail
+
+ Oahu, Hawaii, has some picturesque hikes and this is one of the best!
+
+
+ aae20190509
+ Tue, 27 Nov 2018 01:15:00 +0000
+ 929
+ false
+
+ -
+ full
+ 3
+ 1
+ S01 EP03 Blood Mountain Loop
+
+ Hiking the Appalachian Trail and Freeman Trail in Georgia
+
+
+ aae20190502
+ Tue, 23 Oct 2018 01:15:00 +0000
+ 1440
+ false
+
+ -
+ full
+ 2
+ 1
+ S01 EP02 Garden of the Gods Wilderness
+
+ Wilderness Area Garden of the Gods in Illinois is a delightful spot for
+ an extended hike.
+
+
+ aae20190425
+ Tue, 18 Sep 2018 01:15:00 +0000
+ 839
+ false
+
+ -
+ full
+ 1
+ 1
+ S01 EP01 Upper Priest Lake Trail to Continental Creek Trail
+
+ We check out this powerfully scenic hike following the river in the Idaho
+ Panhandle National Forests.
+
+
+ aae20190418a
+ Tue, 14 Aug 2018 01:15:00 +0000
+ 1399
+ false
+
+
+
diff --git a/data/sample/atom.xml b/data/sample/atom.xml
new file mode 100644
index 0000000..16d3d66
--- /dev/null
+++ b/data/sample/atom.xml
@@ -0,0 +1,19 @@
+
+
+ Example Feed
+
+ 2003-12-13T18:30:02Z
+
+ John Doe
+
+ urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6
+
+
+ Atom-Powered Robots Run Amok
+
+ urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
+ 2003-12-13T18:30:02Z
+ Some text.
+
+
+
diff --git a/data/sample/feedforall.xml b/data/sample/feedforall.xml
new file mode 100644
index 0000000..1181fa3
--- /dev/null
+++ b/data/sample/feedforall.xml
@@ -0,0 +1,151 @@
+
+
+
+ FeedForAll Sample Feed
+ RSS is a fascinating technology. The uses for RSS are expanding daily. Take a closer look at how various industries are using the benefits of RSS in their businesses.
+ http://www.feedforall.com/industry-solutions.htm
+ Computers/Software/Internet/Site Management/Content Management
+ Copyright 2004 NotePage, Inc.
+ http://blogs.law.harvard.edu/tech/rss
+ en-us
+ Tue, 19 Oct 2004 13:39:14 -0400
+ marketing@feedforall.com
+ Tue, 19 Oct 2004 13:38:55 -0400
+ webmaster@feedforall.com
+ FeedForAll Beta1 (0.0.1.8)
+
+ http://www.feedforall.com/ffalogo48x48.gif
+ FeedForAll Sample Feed
+ http://www.feedforall.com/industry-solutions.htm
+ FeedForAll Sample Feed
+ 48
+ 48
+
+ -
+ RSS Solutions for Restaurants
+ <b>FeedForAll </b>helps Restaurant's communicate with customers. Let your customers know the latest specials or events.<br>
+<br>
+RSS feed uses include:<br>
+<i><font color="#FF0000">Daily Specials <br>
+Entertainment <br>
+Calendar of Events </i></font>
+ http://www.feedforall.com/restaurant.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:11 -0400
+
+ -
+ RSS Solutions for Schools and Colleges
+ FeedForAll helps Educational Institutions communicate with students about school wide activities, events, and schedules.<br>
+<br>
+RSS feed uses include:<br>
+<i><font color="#0000FF">Homework Assignments <br>
+School Cancellations <br>
+Calendar of Events <br>
+Sports Scores <br>
+Clubs/Organization Meetings <br>
+Lunches Menus </i></font>
+ http://www.feedforall.com/schools.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:09 -0400
+
+ -
+ RSS Solutions for Computer Service Companies
+ FeedForAll helps Computer Service Companies communicate with clients about cyber security and related issues. <br>
+<br>
+Uses include:<br>
+<i><font color="#0000FF">Cyber Security Alerts <br>
+Specials<br>
+Job Postings </i></font>
+ http://www.feedforall.com/computer-service.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:07 -0400
+
+ -
+ RSS Solutions for Governments
+ FeedForAll helps Governments communicate with the general public about positions on various issues, and keep the community aware of changes in important legislative issues. <b><i><br>
+</b></i><br>
+RSS uses Include:<br>
+<i><font color="#00FF00">Legislative Calendar<br>
+Votes<br>
+Bulletins</i></font>
+ http://www.feedforall.com/government.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:05 -0400
+
+ -
+ RSS Solutions for Politicians
+ FeedForAll helps Politicians communicate with the general public about positions on various issues, and keep the community notified of their schedule. <br>
+<br>
+Uses Include:<br>
+<i><font color="#FF0000">Blogs<br>
+Speaking Engagements <br>
+Statements<br>
+ </i></font>
+ http://www.feedforall.com/politics.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:03 -0400
+
+ -
+ RSS Solutions for Meteorologists
+ FeedForAll helps Meteorologists communicate with the general public about storm warnings and weather alerts, in specific regions. Using RSS meteorologists are able to quickly disseminate urgent and life threatening weather warnings. <br>
+<br>
+Uses Include:<br>
+<i><font color="#0000FF">Weather Alerts<br>
+Plotting Storms<br>
+School Cancellations </i></font>
+ http://www.feedforall.com/weather.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:09:01 -0400
+
+ -
+ RSS Solutions for Realtors & Real Estate Firms
+ FeedForAll helps Realtors and Real Estate companies communicate with clients informing them of newly available properties, and open house announcements. RSS helps to reach a targeted audience and spread the word in an inexpensive, professional manner. <font color="#0000FF"><br>
+</font><br>
+Feeds can be used for:<br>
+<i><font color="#FF0000">Open House Dates<br>
+New Properties For Sale<br>
+Mortgage Rates</i></font>
+ http://www.feedforall.com/real-estate.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:08:59 -0400
+
+ -
+ RSS Solutions for Banks / Mortgage Companies
+ FeedForAll helps <b>Banks, Credit Unions and Mortgage companies</b> communicate with the general public about rate changes in a prompt and professional manner. <br>
+<br>
+Uses include:<br>
+<i><font color="#0000FF">Mortgage Rates<br>
+Foreign Exchange Rates <br>
+Bank Rates<br>
+Specials</i></font>
+ http://www.feedforall.com/banks.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:08:57 -0400
+
+ -
+ RSS Solutions for Law Enforcement
+ <b>FeedForAll</b> helps Law Enforcement Professionals communicate with the general public and other agencies in a prompt and efficient manner. Using RSS police are able to quickly disseminate urgent and life threatening information. <br>
+<br>
+Uses include:<br>
+<i><font color="#0000FF">Amber Alerts<br>
+Sex Offender Community Notification <br>
+Weather Alerts <br>
+Scheduling <br>
+Security Alerts <br>
+Police Report <br>
+Meetings</i></font>
+ http://www.feedforall.com/law-enforcement.htm
+ Computers/Software/Internet/Site Management/Content Management
+ http://www.feedforall.com/forum
+ Tue, 19 Oct 2004 11:08:56 -0400
+
+
+
diff --git a/data/sample/youtube.xml b/data/sample/youtube.xml
new file mode 100644
index 0000000..3ffe622
--- /dev/null
+++ b/data/sample/youtube.xml
@@ -0,0 +1,974 @@
+
+
+
+ yt:channel:
+
+ Critical Role
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2018-05-23T17:03:19+00:00
+
+ yt:video:0_NVdZp8haA
+ 0_NVdZp8haA
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Aurora Grows | Critical Role | Campaign 3, Episode 49
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-20T20:00:01+00:00
+ 2023-02-20T22:24:07+00:00
+
+ The Aurora Grows | Critical Role | Campaign 3, Episode 49
+
+
+ This episode is sponsored by Thorum. Enjoy 20% off your Thorum ring with code Criticalrole at https://Thorum.com
+
+Bells Hells travel the aurora-filled skies of the Hellcatch Valley, concocting plans and gathering allies as the days tick down to the apogee solstice...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:ORUD8YqDp5E
+ ORUD8YqDp5E
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Legend of Vox Machina Season 2, Episodes 10-12 Q&A
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-16T18:00:09+00:00
+ 2023-02-16T18:00:09+00:00
+
+ The Legend of Vox Machina Season 2, Episodes 10-12 Q&A
+
+
+ Season 2 of The Legend of Vox Machina is available now on Prime Video! Watch here: https://amzn.to/3o4nBS5
+
+Join Matthew Mercer, Liam O’Brien, Sam Riegel, and Travis Willingham as well as special guest Phil Bourassa (Character Designer) as we watch Episodes 10-12, answer some burning questions about the series, and celebrate the wrap up of Season 2!
+
+These Q&A segments originally aired as part of a Watch Party moderated by Mica Burton on our Twitch channel.
+The Legend of Vox Machina Watch Parties air every Tuesday following a LVM episode release at 7pm Pacific on Twitch: https://twitch.tv/criticalrole
+
+To learn more about Watch Parties, check out: https://critrole.com/shows/watch-party/
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Mighty Vibes, and Narrative Telephone. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, iTunes & Google Play following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+#TheLegendOfVoxMachina #CriticalRole #WatchParty
+
+
+
+
+
+
+
+ yt:video:wQGk9ZUIOUU
+ wQGk9ZUIOUU
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ An Exit Most Fraught | Critical Role | Campaign 3, Episode 48
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-13T20:00:31+00:00
+ 2023-02-16T17:09:07+00:00
+
+ An Exit Most Fraught | Critical Role | Campaign 3, Episode 48
+
+
+ This episode is sponsored by Skillshare. The first 1,000 Critters to use this link will get a 1 month free trial of Skillshare: https://skl.sh/criticalrole02231
+
+Bells Hells need to leave the Fey Realm behind, but the threats on their scent are extremely unkind...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:h-Df3BQ3FmM
+ h-Df3BQ3FmM
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Legend of Vox Machina Season 2, Episodes 7-9 Q&A
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-09T18:00:14+00:00
+ 2023-02-09T21:09:15+00:00
+
+ The Legend of Vox Machina Season 2, Episodes 7-9 Q&A
+
+
+ Season 2 of The Legend of Vox Machina is available now on Prime Video! Watch here: https://amzn.to/3o4nBS5
+
+Join Laura Bailey, Taliesin Jaffe, Ashley Johnson, Sam Riegel, and Travis Willingham as well as special guests Troy Baker (Voice of Syldor Vessar), Young Heller (Episodic Director), and Arthur Loftis (Art Director) as we watch Episodes 7-9 and answer some burning questions about the series!
+
+These Q&A segments originally aired as part of a Watch Party moderated by Mica Burton on our Twitch channel.
+The Legend of Vox Machina Watch Parties air every Tuesday following a LVM episode release at 7pm Pacific on Twitch: https://twitch.tv/criticalrole
+
+To learn more about Watch Parties, check out: https://critrole.com/shows/watch-party/
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Mighty Vibes, and Narrative Telephone. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, iTunes & Google Play following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+#TheLegendOfVoxMachina #CriticalRole #WatchParty
+
+
+
+
+
+
+
+ yt:video:yGzf-LmpWz8
+ yGzf-LmpWz8
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Fey Key | Critical Role | Campaign 3, Episode 47
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-06T20:00:17+00:00
+ 2023-02-13T20:30:41+00:00
+
+ The Fey Key | Critical Role | Campaign 3, Episode 47
+
+
+ This episode is sponsored by Nord VPN. Exclusive! Grab the NordVPN deal ➼ https://nordvpn.com/criticalrole. Try it risk-free now with a 30-day money-back guarantee!
+
+Bells Hells carefully traverse the shadowy side of the Fey Realm and dodge numerous dangers in their attempt to destroy the first Malleus Key…
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:2DWZO-J4zps
+ 2DWZO-J4zps
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Legend of Vox Machina Season 2, Episodes 4-6 Q&A
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-02-02T18:00:03+00:00
+ 2023-02-02T18:00:03+00:00
+
+ The Legend of Vox Machina Season 2, Episodes 4-6 Q&A
+
+
+ Season 2 of The Legend of Vox Machina is available now on Prime Video! Watch here: https://amzn.to/3o4nBS5
+
+Join Liam O’Brien, Marisha Ray, Sam Riegel, and Travis Willingham as well as special guests Robbie Daymond (Voice of Cerkonos) and Alicia Chan (Episodic Director) as we watch Episodes 4-6 and answer some burning questions about the series!
+
+These Q&A segments originally aired as part of a Watch Party moderated by Mica Burton on our Twitch channel.
+The Legend of Vox Machina Watch Parties air every Tuesday following a LVM episode release at 7pm Pacific on Twitch: https://twitch.tv/criticalrole
+
+To learn more about Watch Parties, check out: https://critrole.com/shows/watch-party/
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Mighty Vibes, and Narrative Telephone. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, iTunes & Google Play following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+#TheLegendOfVoxMachina #CriticalRole #WatchParty
+
+
+
+
+
+
+
+ yt:video:1knIHqLqThk
+ 1knIHqLqThk
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ Night at the Ligament Manor | Critical Role | Campaign 3, Episode 46
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-30T20:00:00+00:00
+ 2023-02-02T01:49:21+00:00
+
+ Night at the Ligament Manor | Critical Role | Campaign 3, Episode 46
+
+
+ This episode is sponsored by Persona 3 Portable and Persona 4 Golden. Persona 3 Portable and Persona 4 Golden are available now! Learn more at https://persona.atlus.com/
+
+This episode is sponsored by Skillshare. The first 1,000 critters to use this link will get a 1 month free trial of Skillshare: https://skl.sh/criticalrole12221
+
+Bells Hells travel to the Fey Realm, where they meet Fearne's curious Nana Morri and learn more about Fearne's unconventional upbringing...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:eRcD8chte08
+ eRcD8chte08
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ A Challenger Approaches... Marisha Ray joins Creator Clash 2! #Shorts
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-27T18:00:12+00:00
+ 2023-02-04T14:01:27+00:00
+
+ A Challenger Approaches... Marisha Ray joins Creator Clash 2! #Shorts
+
+
+ Learn more at https://thecreatorclash.com/#charity
+
+
+
+
+
+
+
+ yt:video:1A2JVqUt1T8
+ 1A2JVqUt1T8
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The Legend of Vox Machina Season 2, Episodes 1-3 Q&A
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-26T18:00:00+00:00
+ 2023-02-13T04:50:23+00:00
+
+ The Legend of Vox Machina Season 2, Episodes 1-3 Q&A
+
+
+ Season 2 of The Legend of Vox Machina is available now on Prime Video! Watch here: https://amzn.to/3o4nBS5
+
+Matthew Mercer, Sam Riegel, Travis Willingham, and special guests Will Friedle (voice of Kashaw), Sung Jin (Supervising Director at Titmouse), and Sunil Malhotra (voice of Shaun Gilmore), answer questions about Episodes 1-3 of Season 2 for The Legend of Vox Machina!
+
+These Q&A segments originally aired as part of a Watch Party moderated by Mica Burton on our Twitch channel.
+The Legend of Vox Machina Watch Parties aired every Tuesday following a LVM episode release at 7pm Pacific on Twitch: https://twitch.tv/criticalrole
+
+To learn more about Watch Parties, check out: https://critrole.com/shows/watch-party/
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Mighty Vibes, and Narrative Telephone. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, iTunes & Google Play following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+#TheLegendOfVoxMachina #CriticalRole #WatchParty
+
+00:00:00 Welcome to the Watch Party!
+00:03:19 Episode 1 Q&A
+00:18:28 Episode 2 Q&A
+00:38:01 Episode 3 Q&A
+
+
+
+
+
+
+
+ yt:video:IZ39H9H_y5Q
+ IZ39H9H_y5Q
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ #EverythingIsContent - Persona 3 Portable
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-25T18:00:09+00:00
+ 2023-01-25T18:00:09+00:00
+
+ #EverythingIsContent - Persona 3 Portable
+
+
+ This episode is sponsored by Persona 3 Portable.
+
+Our very own Taliesin Jaffe and Liam O'Brien show off and play the new release of Persona 3 Portable in this sponsored episode of #EverythingIsContent.
+
+Persona 3 Portable and Persona 4 Golden are available now! Learn more at https://persona.atlus.com/
+
+Twitch/Twitter/FB/IG: @atlus_west
+YouTube: @atlus
+
+This live stream originally aired Monday, January 23rd, 2023 on https://twitch.tv/criticalrole
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, 4-Sided Dive, and #EverythingIsContent. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, iTunes & Google Play following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #Persona3 #Persona4 #EverythingIsContent
+
+
+
+
+
+
+
+ yt:video:7t2NJJjy8r8
+ 7t2NJJjy8r8
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ Ominous Lectures | Critical Role | Campaign 3, Episode 45
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-16T20:00:02+00:00
+ 2023-01-18T21:12:16+00:00
+
+ Ominous Lectures | Critical Role | Campaign 3, Episode 45
+
+
+ This episode is sponsored by NordVPN. Exclusive! Grab the NordVPN deal ➼ https://nordvpn.com/criticalrole. Try it risk-free now with a 30-day money-back guarantee!
+
+This episode is sponsored by Cash App. Download Cash App from the App Store or Google Play store today to create your own $cashtag.
+
+This episode is sponsored by D&D Beyond. Become a D&D Beyond subscriber today and get exclusive monthly perks https://dndbeyond.link/CRsub
+
+You can find more details about The Super Awesome Contest To Become The Next Big Voice Actor on the I Hear Voices Podcast Instagram here: https://www.instagram.com/p/CnLnJ5jL_jG/?hl=en
+
+Bells Hells return to the Aydinlan Seminary to speak with two professors about the mysteries affecting their friends, but an unexpected guest complicates everything...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:nqXmeYBqF1c
+ nqXmeYBqF1c
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ Bawdy Basement Belligerence | Critical Role | Campaign 3, Episode 44
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-09T20:00:21+00:00
+ 2023-02-16T11:41:37+00:00
+
+ Bawdy Basement Belligerence | Critical Role | Campaign 3, Episode 44
+
+
+ This episode is sponsored by Capital One Shopping. Avoid Paying Full Price. Get Capital One Shopping for FREE - whether you’re a Capital One customer or not. Go to https://capitaloneshopping.com/criticalrole to install the extension today.
+
+This episode is sponsored by D&D Beyond. Add 11 new Dragonlance Monsters at no cost with Monsterous Compendium Volume 2 https://dndbeyond.link/CRvol2.
+
+Trapped in the basement of a fugitive, Bells Hells turn to chaos in an effort to escape their hunters and learn more about who leads their adversaries...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:7gRiV02TWww
+ 7gRiV02TWww
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ To Be Continued! | 4-Sided Dive | Episode 10: Discussing Up To C3E43 & M9 Reunited
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2023-01-04T20:00:02+00:00
+ 2023-02-01T04:29:30+00:00
+
+ To Be Continued! | 4-Sided Dive | Episode 10: Discussing Up To C3E43 & M9 Reunited
+
+
+ Ashley Johnson, Liam O’Brien, Sam Riegel, and Travis Willingham are starting off the new year with a new episode of 4-Sided Dive! What will be pulled from the Tower of Inquiry? Will Sam ever improve his memory? Who will be the Quiplash champion? You'll have to watch to find out!
+
+4-Sided Dive airs one Tuesday a month on http://twitch.tv/criticalrole and http://youtube.com/criticalrole
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, 4-Sided Dive, and Exandria Unlimited. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+"Let's Roll (4-Sided Dive Theme)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #4SD #BellsHells #MightyNein
+
+
+
+
+
+
+
+ yt:video:cGhVgjYSpIE
+ cGhVgjYSpIE
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ Axiom Shaken | Critical Role | Campaign 3, Episode 43
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2022-12-26T20:00:16+00:00
+ 2022-12-28T17:38:43+00:00
+
+ Axiom Shaken | Critical Role | Campaign 3, Episode 43
+
+
+ This episode is sponsored by Cash App. Download Cash App from the App Store or Google Play store today to create your own $cashtag.
+
+This episode is sponsored by Battle Spirits Saga. Follow and comment what you would do if you won part of the cash prize on @BSS_TCG pinned tweet with #BSSxCriticalRole to be entered into a random drawing for one of the 5 card test pressings! https://bit.ly/BSSCRIT - ©BNP/BANDAI
+
+Bells Hells seek out answers about the stolen texts from members of the Grim Verity, but the knowledge revealed could change everything they know about the gods of Exandria…
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
+ yt:video:1OHsbXHMUs4
+ 1OHsbXHMUs4
+ UCpXBGqwsBkpvcYjsJBQ7LEQ
+ The City of Flowing Light | Critical Role | Campaign 3, Episode 42
+
+
+ Critical Role
+ https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ
+
+ 2022-12-19T20:00:00+00:00
+ 2023-02-13T10:47:55+00:00
+
+ The City of Flowing Light | Critical Role | Campaign 3, Episode 42
+
+
+ This episode is sponsored by Cash App. Download Cash App from the App Store or Google Play store today to create your own $cashtag.
+
+This episode is sponsored by D&D Beyond. Give the gift of D&D by gifting your friends books on D&D Beyond. https://dndbeyond.link/CRshop
+
+Bells Hells depart from their new allies and head to Yios, where they are quickly dazzled by the lake-top metropolis' glamor and gambling...
+
+CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+
+Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+
+Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+
+"It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+Original Music by Omar Fadel and Hexany Audio
+"Welcome to Marquet" Art Theme by Colm McGuinness
+Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+Character Art by Hannah Friederichs
+
+Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+
+
+Follow us!
+Website: https://www.critrole.com
+Newsletter: https://critrole.com/newsletter
+Facebook: https://www.facebook.com/criticalrole
+Twitter: https://twitter.com/criticalrole
+Instagram: https://instagram.com/critical_role
+Twitch: https://www.twitch.tv/criticalrole
+
+Shops:
+US: https://shop.critrole.com
+UK: https://shop.critrole.co.uk
+EU: https://shop.critrole.eu
+AU: https://shop.critrole.com.au
+CA: https://canada.critrole.com
+
+Follow Critical Role Foundation!
+Learn More & Donate: https://criticalrolefoundation.org
+Twitter: https://twitter.com/CriticalRoleFDN
+Facebook: https://facebook.com/CriticalRoleFDN
+
+Want games? Follow Darrington Press
+Newsletter: https://darringtonpress.com/newsletter
+Twitter: https://twitter.com/DarringtonPress
+Facebook: https://www.facebook.com/darringtonpress
+
+Check out our animated series!
+The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+
+#CriticalRole #BellsHells #DungeonsAndDragons
+
+
+
+
+
+
+
diff --git a/data/url_atom.xml b/data/url_atom.xml
index a5529b6..90e2d92 100644
--- a/data/url_atom.xml
+++ b/data/url_atom.xml
@@ -1,15 +1,15 @@
-Sample Feed
-For documentation only
-
-
-First entry title
-
-
-Mark Pilgrim
-../about/
-mark@example.org
-
-
+ Sample Feed
+ For documentation only
+
+
+ First entry title
+
+
+ Mark Pilgrim
+ ../about/
+ mark@example.org
+
+
diff --git a/data/url_atom2.xml b/data/url_atom2.xml
index 639f85f..f8689fe 100644
--- a/data/url_atom2.xml
+++ b/data/url_atom2.xml
@@ -1,15 +1,15 @@
-Sample Feed
-For documentation only
-
-
-First entry title
-
-
-Mark Pilgrim
-../about/
-mark@example.org
-
-
+ Sample Feed
+ For documentation only
+
+
+ First entry title
+
+
+ Mark Pilgrim
+ ../about/
+ mark@example.org
+
+
diff --git a/lua/feed/commands.lua b/lua/feed/commands.lua
index b8cd5ba..a9f722f 100644
--- a/lua/feed/commands.lua
+++ b/lua/feed/commands.lua
@@ -196,7 +196,7 @@ M.update = {
local prog = Progress.new(#ut.feedlist(db.feeds, false))
vim.system({ "nvim", "--headless", "-c", 'lua require"feed.fetch".update_all()' }, {
text = true,
- stderr = function(_, data)
+ stdout = function(_, data)
if data and vim.trim(data) ~= "" then
prog:update(vim.trim(data))
end
diff --git a/lua/feed/config.lua b/lua/feed/config.lua
index 9fc557a..181f7e4 100644
--- a/lua/feed/config.lua
+++ b/lua/feed/config.lua
@@ -60,7 +60,7 @@ local default = {
},
search = {
- default_query = "@6-months-ago +unread ",
+ default_query = "@2-weeks-ago +unread ",
backend = {
"mini.pick",
"telescope",
diff --git a/lua/feed/fetch.lua b/lua/feed/fetch.lua
index 82f3c06..509a7ca 100644
--- a/lua/feed/fetch.lua
+++ b/lua/feed/fetch.lua
@@ -2,11 +2,9 @@ local Coop = require("coop")
local Feedparser = require("feed.parser")
local Curl = require("feed.curl")
local Config = require("feed.config")
-local Markdown = require("feed.ui.markdown")
local db = require("feed.db")
local ut = require("feed.utils")
local M = {}
-local as_completed = require("coop.control").as_completed
local valid_response = ut.list2lookup({ 200, 301, 302, 303, 304, 307, 308 })
local encoding_blacklist = ut.list2lookup({ "gb2312" })
@@ -83,17 +81,27 @@ function M.update_all()
local list = ut.feedlist(feeds, false)
local n = #list
+ local io_print = function(...)
+ local content = table.concat({ ... }, " ")
+ io.write(content)
+ end
+
+ if n == 0 then
+ io_print("Empty database\n")
+ os.exit()
+ end
+
for i = 1, n do
Coop.spawn(function()
local url = list[i]
local ok = M.update_feed_co(url, { force = false })
local name = ut.url2name(url, feeds)
c = c + 1
- print(name, ok and "success" or "failed")
+ io_print(name, ok and "success" or "failed")
if c == n then
os.exit()
end
- print("\n")
+ io_print("\n")
end)
end
end
diff --git a/lua/feed/health.lua b/lua/feed/health.lua
index 37dca08..3695bb2 100644
--- a/lua/feed/health.lua
+++ b/lua/feed/health.lua
@@ -15,6 +15,7 @@ local dependencies = {
local plugins = {
{ lib = "coop", optional = false, info = "required for concurrency" },
+ { lib = "snacks", optional = true, info = "required for image rendering" },
}
local parsers = {
diff --git a/lua/feed/integrations/rsshub.lua b/lua/feed/integrations/rsshub.lua
index 76aa9a3..9bfb9d8 100644
--- a/lua/feed/integrations/rsshub.lua
+++ b/lua/feed/integrations/rsshub.lua
@@ -1,4 +1,4 @@
-local Config = require("feed.config")
-return function(url)
- return url:find("rsshub:/") and url:gsub("rsshub:/", Config.rsshub.instance) .. "?format=json?mode=fulltext" or url
+return function(url, instance)
+ instance = instance or require("feed.config").rsshub.instance
+ return url:find("rsshub:/") and url:gsub("rsshub:/", instance) .. "?format=json?mode=fulltext" or url
end
diff --git a/lua/feed/opml.lua b/lua/feed/opml.lua
index a26caeb..50efec1 100644
--- a/lua/feed/opml.lua
+++ b/lua/feed/opml.lua
@@ -2,13 +2,13 @@ local M = {}
local ut = require("feed.utils")
local xml = require("feed.parser.xml")
-local format, concat = string.format, table.concat
+local format, concat, insert = string.format, table.concat, table.insert
local spairs, ipairs = vim.spairs, ipairs
---@param src string
---@return feed.opml?
function M.import(src)
- local ok, ast = pcall(xml.parse, src, "")
+ local ast = xml.parse(src, "")
local ret = {}
local function handle(node, tags)
@@ -19,7 +19,7 @@ function M.import(src)
v.tags = {}
end
for _, t in ipairs(tags) do
- table.insert(v.tags, t)
+ insert(v.tags, t)
end
end
if v.xmlUrl then
@@ -32,12 +32,12 @@ function M.import(src)
if not v.tags then
v.tags = {}
end
- table.insert(v.tags, v.text)
+ insert(v.tags, v.text)
handle(v, v.tags)
end
end
end
- if ok and ast then
+ if ast then
handle(ast.opml.body)
return ret
end
@@ -46,39 +46,33 @@ end
---@param t table
---@return string
local function format_outline(t)
- local buf = {}
- for k, v in pairs(t) do
- buf[#buf + 1] = format([[%s="%s"]], k, v)
- end
- return ""
-end
-
-local root_format = [[
-%s
-%s
-]]
-
-local function rsshub_replace(url)
- local Config = require("feed.config")
- return url:find("rsshub:/") and url:gsub("rsshub:/", Config.rsshub.export) or url
+ local acc = vim.iter(t):fold({}, function(acc, k, v)
+ insert(acc, format('%s="%s"', k, v))
+ return acc
+ end)
+ return format("", concat(acc, " "))
end
---@param feeds feed.opml
---@return string
function M.export(feeds)
- local buf = {}
- for xmlUrl, v in spairs(feeds) do
- if type(v) == "table" then
- buf[#buf + 1] = format_outline({
- text = v.description or v.title,
- title = v.title,
- htmlUrl = v.htmlUrl,
- xmlUrl = rsshub_replace(xmlUrl),
+ local root = [[
+%s
+%s
+]]
+ local acc = vim.iter(spairs(feeds)):fold({}, function(acc, xmlUrl, feed)
+ if type(feed) == "table" then
+ acc[#acc + 1] = format_outline({
+ text = feed.description or feed.title,
+ title = feed.title,
+ htmlUrl = feed.htmlUrl,
+ xmlUrl = require("feed.integrations.rsshub")(xmlUrl, require("feed.config").rsshub.export),
type = "rss",
})
end
- end
- return format(root_format, "feed.nvim export", concat(buf, "\n"))
+ return acc
+ end)
+ return format(root, "feed.nvim export", concat(acc, "\n"))
end
return M
diff --git a/lua/feed/parser/atom.lua b/lua/feed/parser/atom.lua
index 56acd7d..a52b4db 100644
--- a/lua/feed/parser/atom.lua
+++ b/lua/feed/parser/atom.lua
@@ -1,8 +1,14 @@
local date = require("feed.parser.date")
local ut = require("feed.utils")
-local p_ut = require("feed.parser.utils")
-local sensible = p_ut.sensible
-local decode = require("feed.lib.entities").decode
+local sensible = ut.sensible
+local decode = ut.decode
+
+---@param str string?
+---@return string?
+local clean = function(str)
+ str = decode(str)
+ return str and vim.trim(str) or nil
+end
local function handle_version(ast)
if ast.feed.version == "1.0" or not ast.feed.version then
@@ -17,15 +23,17 @@ local function handle_link(ast, base)
local T = type(ast.link)
base = ut.url_rebase(ast, base)
if T == "table" then
- for _, v in ipairs(ut.listify(ast.link)) do
+ local list = ut.listify(ast.link)
+ for _, v in ipairs(list) do
if v.rel == "alternate" then
- return ut.url_resolve(base, v.href)
- -- elseif v.rel == "self" then
- -- return ut.url_resolve(base, v.href)
+ if not ut.looks_like_url(v.href) then
+ return ut.url_resolve(base, v.href)
+ else
+ return v.href
+ end
end
end
- return base
- -- return ut.url_resolve(base, ast.link[1].href) -- just in case..?
+ return ut.url_resolve(list[1].href)
elseif T == "string" then
return ast.link
end
@@ -55,7 +63,7 @@ local function handle_content(entry, fallback)
if not content then
return fallback
end
- return content[1]
+ return clean(content[1])
end
local function handle_date(entry)
@@ -64,31 +72,38 @@ local function handle_date(entry)
end
local function handle_feed_title(ast, url)
- return sensible(ast.title, 1, url)
+ return decode(sensible(ast.title, 1, url))
end
---@return string?
-local function handle_author(entry, fallback)
- return sensible(entry.author, "name", fallback)
+local function handle_author(node)
+ local author_node = node.author or node["itunes:author"]
+ if not author_node then
+ return
+ end
+ return clean(author_node.name[1])
end
local function handle_description(feed)
- if feed.subtitle then
- return sensible(feed.subtitle, 1)
- else
- return handle_feed_title(feed)
+ if not feed.subtitle then
+ return
end
+ return decode(vim.trim(sensible(feed.subtitle, 1)))
end
-local function handle_entry(entry, base, feed_name, url_id)
+---@param entry table
+---@param feed feed.feed
+---@param base string
+---@return table
+local handle_entry = function(entry, feed, base, url)
local res = {}
local entry_base = ut.url_rebase(entry, base)
res.link = handle_link(entry, entry_base)
res.time = handle_date(entry)
- res.title = decode(handle_title(entry, "no title"))
- res.author = decode(handle_author(entry, feed_name))
+ res.title = handle_title(entry, "no title")
+ res.author = handle_author(entry)
res.content = handle_content(entry, "")
- res.feed = url_id
+ res.feed = url
return res
end
@@ -98,13 +113,14 @@ local function handle_atom(ast, url)
res.version = handle_version(ast)
local root_base = ut.url_rebase(feed, url)
res.link = handle_link(feed, root_base)
- res.desc = decode(handle_description(feed))
- res.title = decode(handle_feed_title(feed, res.link))
+ res.desc = handle_description(feed)
+ res.title = handle_feed_title(feed, res.link)
+ res.author = handle_author(feed)
res.entries = {}
res.type = "atom"
if feed.entry then
for _, v in ipairs(ut.listify(feed.entry)) do
- res.entries[#res.entries + 1] = handle_entry(v, root_base, res.title, url)
+ res.entries[#res.entries + 1] = handle_entry(v, res, root_base, url)
end
end
return res
diff --git a/lua/feed/parser/date.lua b/lua/feed/parser/date.lua
index 17eeab3..a0d4252 100644
--- a/lua/feed/parser/date.lua
+++ b/lua/feed/parser/date.lua
@@ -1,69 +1,65 @@
local M = {}
----@class feed._date
----@field year integer
----@field month integer
----@field day integer
-
----@param date feed._date?
----@return integer
-local os_time = function(date)
- return os.time(date)
-end
-
---@param n integer
----@param now feed._date?
+---@param now integer?
---@return integer
----@private
local function days_ago(n, now)
- now = os_time(now) or os.time()
+ now = now or os.time()
local day = 24 * 60 * 60
return now - day * n
end
---@param n integer
----@param now? feed._date
+---@param now integer?
+---@return integer
+local function weeks_ago(n, now)
+ return days_ago(n * 7, now)
+end
+
+---@param n integer
+---@param now integer?
---@return integer
----@private
local function years_ago(n, now)
- now = now or os.date("*t")
- return os.time { year = now.year - n, month = now.month, day = now.day }
+ local t = os.date("*t", now) or os.date("*t")
+ return os.time({ year = t.year - n, month = t.month, day = t.day })
end
---@param n integer
----@param now? feed._date
+---@param now integer?
---@return integer
local function months_ago(n, now)
- now = now or os.date("*t")
- now.month = now.month - n
+ local t = os.date("*t", now) or os.date("*t")
+
+ t.month = t.month - n
- while now.month < 1 do
- now.month = now.month + 12
- now.year = now.year - 1
+ while t.month < 1 do
+ t.month = t.month + 12
+ t.year = t.year - 1
end
local last_day = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
- if now.year % 4 == 0 and (now.year % 100 ~= 0 or now.year % 400 == 0) then -- Check for leap year
+ if t.year % 4 == 0 and (t.year % 100 ~= 0 or t.year % 400 == 0) then -- Check for leap year
last_day[2] = 29
end
-- Adjust the day if it exceeds the number of days in the new month
- if now.day > last_day[now.month] then
- now.day = last_day[now.month]
+ if t.day > last_day[t.month] then
+ t.day = last_day[t.month]
end
---@diagnostic disable-next-line: param-type-mismatch
- return os.time(now)
+ return os.time(t)
end
M._days_ago = days_ago
M._years_ago = years_ago
+M._weeks_ago = weeks_ago
M._months_ago = months_ago
local patterns = {}
local months =
-{ Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12 }
+ { Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6, Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12 }
local weekdays = { Sun = 1, Mon = 2, Tue = 3, Wed = 4, Thu = 5, Fri = 6, Sat = 7 }
do
@@ -79,34 +75,33 @@ do
local zone = (S("+-") * digit) + C(R("AZ") ^ 1)
local min_and_sec = L.digit ^ 2 * P(":") * L.digit ^ 2 * P("-")
patterns.RFC2822 = alpha
- * P(", ")
- * digit
- * ws
- * alpha
- * ws
- * digit
- * ws
- * digit
- * col
- * digit
- * col
- * digit
- * ws
- * zone
+ * P(", ")
+ * digit
+ * ws
+ * alpha
+ * ws
+ * digit
+ * ws
+ * digit
+ * col
+ * digit
+ * col
+ * digit
+ * ws
+ * zone
patterns.RFC3339 = digit
- * P("-")
- * digit
- * P("-")
- * digit
- * S("Tt")
- * digit
- * (P(":") * min_and_sec ^ -1)
- * digit
- * (P(":") ^ -1)
- * (digit ^ -1)
- * (R("AZ") ^ -1)
- patterns.ASCTIME = alpha * ws * alpha * ws * digit * ws * digit * ws * digit * col * digit * col * digit *
- ws -- TODO: zone
+ * P("-")
+ * digit
+ * P("-")
+ * digit
+ * S("Tt")
+ * digit
+ * (P(":") * min_and_sec ^ -1)
+ * digit
+ * (P(":") ^ -1)
+ * (digit ^ -1)
+ * (R("AZ") ^ -1)
+ patterns.ASCTIME = alpha * ws * alpha * ws * digit * ws * digit * ws * digit * col * digit * col * digit * ws -- TODO: zone
end
---@param str string
@@ -178,12 +173,14 @@ M.parse = function(str, t)
return os.time()
end
-function M.literal(str)
+local function literal(str)
local a, unit, ago = unpack(vim.split(str, "-"))
local n = tonumber(a)
if n and unit and ago then
if unit:find("day") then
return days_ago(n)
+ elseif unit:find("week") then
+ return weeks_ago(n)
elseif unit:find("month") then
return months_ago(n)
elseif unit:find("year") then
@@ -196,15 +193,16 @@ end
---@return integer?
local function numeral(str)
local a, b, c = unpack(vim.split(str, "-"))
- if a and b and c then
- return os.time({ year = tonumber(a), month = tonumber(b), day = tonumber(c) })
+ local an, bn, cn = tonumber(a), tonumber(b), tonumber(c)
+ if an and bn and cn then
+ return os.time({ year = an, month = bn, day = cn })
end
end
---@param str string
---@return integer?
local function filter_part(str)
- for _, f in ipairs({ M.literal, numeral }) do
+ for _, f in ipairs({ literal, numeral }) do
if f(str) then
return f(str)
end
diff --git a/lua/feed/parser/init.lua b/lua/feed/parser/init.lua
index 6ecc602..3da74b9 100644
--- a/lua/feed/parser/init.lua
+++ b/lua/feed/parser/init.lua
@@ -1,5 +1,5 @@
-local xml = require "feed.parser.xml"
-local log = require "feed.lib.log"
+local xml = require("feed.parser.xml")
+local log = require("feed.lib.log")
---@alias feed.type "rss" | "atom" | "json"
---@alias feed.opml table
@@ -8,13 +8,15 @@ local log = require "feed.lib.log"
---@class feed.feed
---@field title? string
---@field entries? feed.entry[] -> nil
----@field description? string -> title?
+---@field desc? string -> title?
---@field htmlUrl? string
+---@field link? string
---@field type? feed.type
---@field tags? string[]
---@field last_modified? string
---@field etag? string
---@field version? feed.version
+---@field author? string
---@class feed.entry
---@field feed string url to the feed
@@ -25,9 +27,9 @@ local log = require "feed.lib.log"
---@field content? string -> ""
---@field tags? table
-local handle_rss = require "feed.parser.rss"
-local handle_atom = require "feed.parser.atom"
-local handle_json = require "feed.parser.jsonfeed"
+local handle_rss = require("feed.parser.rss")
+local handle_atom = require("feed.parser.atom")
+local handle_json = require("feed.parser.jsonfeed")
local M = {}
@@ -41,9 +43,9 @@ function M.parse(src, url)
else
local ast = xml.parse(src, url)
if ast then
- if ast['rss'] or ast["rdf:RDF"] then
+ if ast["rss"] or ast["rdf:RDF"] then
return handle_rss(ast, url)
- elseif ast['feed'] then
+ elseif ast["feed"] then
return handle_atom(ast, url)
else
log.warn(url, "unknown feedtype")
diff --git a/lua/feed/parser/jsonfeed.lua b/lua/feed/parser/jsonfeed.lua
index 6f9bbf7..55d07c8 100644
--- a/lua/feed/parser/jsonfeed.lua
+++ b/lua/feed/parser/jsonfeed.lua
@@ -1,43 +1,45 @@
local date = require("feed.parser.date")
local ut = require("feed.utils")
-local p_ut = require("feed.parser.utils")
-local sensible = p_ut.sensible
-local decode = require("feed.lib.entities").decode
+local sensible = ut.sensible
+local decode = ut.decode
-local function handle_title(entry)
- if not entry.title then
+local function handle_title(node)
+ if not node.title then
return "no title"
end
- return entry.title
+ return decode(node.title)
end
-local function handle_entry(entry, author, feed_name, feed_url, url_id)
+local function handle_author(ast, feed_name)
+ return sensible(ast.author, "name", feed_name)
+end
+
+---@param entry table
+---@param feed feed.feed
+---@return table
+local handle_entry = function(entry, feed, url)
local res = {}
res.link = entry.url
res.content = entry.content_html or ""
res.time = date.parse(entry.date_published, "json")
- res.title = decode(handle_title(entry))
- res.author = decode(author)
- res.feed = url_id
+ res.title = handle_title(entry)
+ res.author = decode(feed.author)
+ res.feed = url
return res
end
-local function handle_author(ast, feed_name)
- return sensible(ast.author, "name", feed_name)
-end
-
return function(ast, url) -- no link resolve for now only do html link resolve later
local res = {}
res.version = "json1"
- res.link = ast.home_page_url or ast.feed_url
- res.title = decode(ast.title)
+ res.link = ast.home_page_url or ast.feed_url or url
+ res.title = handle_title(ast)
res.desc = decode(ast.description or res.title)
res.author = decode(handle_author(ast, res.title))
res.entries = {}
res.type = "json"
if ast.items then
for _, v in ipairs(ut.listify(ast.items)) do
- res.entries[#res.entries + 1] = handle_entry(v, res.author, res.title, res.link, url)
+ res.entries[#res.entries + 1] = handle_entry(v, res, url)
end
end
return res
diff --git a/lua/feed/parser/rss.lua b/lua/feed/parser/rss.lua
index df2f96b..9ca6ade 100644
--- a/lua/feed/parser/rss.lua
+++ b/lua/feed/parser/rss.lua
@@ -1,8 +1,14 @@
local date = require("feed.parser.date")
local ut = require("feed.utils")
-local p_ut = require("feed.parser.utils")
-local sensible = p_ut.sensible
-local decode = require("feed.lib.entities").decode
+local sensible = ut.sensible
+local decode = ut.decode
+
+---@param str string?
+---@return string?
+local clean = function(str)
+ str = decode(str)
+ return str and vim.trim(str) or nil
+end
local function handle_version(ast)
local version
@@ -57,8 +63,11 @@ local function handle_link(node, base) -- TODO: base and rebase modified for rss
return base
end
-local function handle_author(node, fallback)
- return sensible(node["author"] or node["dc:creator"] or node["itunes:author"], 1, fallback)
+local function handle_author(node)
+ local author_node = node["itunes:author"] or node["author"] or node["dc:creator"] or node["dc:author"]
+ if author_node then
+ return clean(author_node[1] or author_node.name)
+ end
end
local function handle_date(entry)
@@ -67,10 +76,9 @@ local function handle_date(entry)
end
local function handle_content(entry, fallback)
- -- TODO: type of content not relevant?
- local content = sensible(entry["content:encoded"] or entry.description, 1)
+ local content = sensible(entry["content:encoded"] or entry["description"], 1)
if content then
- return content
+ return clean(content)
else
return fallback
end
@@ -81,33 +89,33 @@ local function handle_entry_title(entry, fallback)
end
local function handle_description(channel, fallback)
- return sensible(channel.description or channel["dc:description"] or channel["itunes:subtitle"], 1, fallback)
+ return clean(sensible(channel.description or channel["dc:description"] or channel["itunes:subtitle"], 1, fallback))
end
-local function handle_entry(entry, feed_url, feed_name, feed_author, url_id)
+local function handle_entry(entry, feed, url)
local res = {}
- res.link = handle_link(entry, feed_url)
+ res.link = handle_link(entry, feed.link)
res.content = handle_content(entry, "")
res.title = decode(handle_entry_title(entry, "no title"))
res.time = handle_date(entry)
- res.author = decode(handle_author(entry, feed_author or feed_name))
- res.feed = url_id
+ res.author = handle_author(entry) or feed.author
+ res.feed = url
return res
end
-local function handle_rss(ast, url_id)
+local function handle_rss(ast, url)
local res = {}
res.version = handle_version(ast)
local channel = ast.rss and ast.rss.channel or ast["rdf:RDF"].channel
- res.link = handle_link(channel, url_id)
+ res.link = handle_link(channel, url)
res.title = decode(handle_title(channel, res.link))
- local feed_author = decode(handle_author(channel, res.title))
+ res.author = handle_author(channel)
res.desc = decode(handle_description(channel, res.title))
res.entries = {}
res.type = "rss"
if channel.item then
for _, v in ipairs(ut.listify(channel.item)) do
- res.entries[#res.entries + 1] = handle_entry(v, res.link, res.title, feed_author, url_id)
+ res.entries[#res.entries + 1] = handle_entry(v, res, url)
end
end
return res
diff --git a/lua/feed/parser/utils.lua b/lua/feed/parser/utils.lua
deleted file mode 100644
index b90656a..0000000
--- a/lua/feed/parser/utils.lua
+++ /dev/null
@@ -1,30 +0,0 @@
-local M = {}
-
----@param thing table | string
----@param field string | integer
----@return string
-function M.sensible(thing, field, fallback)
- if not thing then
- return fallback
- end
- if type(thing) == "table" then
- --- TODO: handle if list
- if vim.tbl_isempty(thing) then
- return fallback
- elseif type(thing[field]) == "string" then
- return thing[field]
- else
- return fallback
- end
- elseif type(thing) == "string" then
- if thing == "" then
- return fallback
- else
- return thing
- end
- else
- return fallback
- end
-end
-
-return M
diff --git a/lua/feed/parser/xml.lua b/lua/feed/parser/xml.lua
index 4c65bff..4e4058b 100644
--- a/lua/feed/parser/xml.lua
+++ b/lua/feed/parser/xml.lua
@@ -234,7 +234,7 @@ end
---@param src string
---@param url string
---@return table?
-local function parse(src, url)
+local parse = vim.F.nil_wrap(function(src, url)
ut.assert_parser("xml")
src = sanitize(src)
local root = get_root(src, "xml")
@@ -249,7 +249,7 @@ local function parse(src, url)
collected[2].encoding = collected[1].encoding
end
return #collected == 2 and collected[2] or collected[1]
-end
+end)
return {
parse = parse,
diff --git a/lua/feed/ui.lua b/lua/feed/ui.lua
index 455eff5..ad757dc 100644
--- a/lua/feed/ui.lua
+++ b/lua/feed/ui.lua
@@ -101,8 +101,8 @@ local function render_entry(buf, body, id)
header[i] = ut.capticalize(v) .. ": " .. Format[v](id)
end
- local ok, urls = pcall(ut.get_urls, body, db[id].link)
- if ok then
+ local urls = ut.get_urls(body, db[id].link)
+ if urls then
state.urls = urls
else
if vim.g.feed_debug then
@@ -110,8 +110,13 @@ local function render_entry(buf, body, id)
end
end
+ local ok, res
+
for _, f in ipairs(body_transforms) do
- body = f(body, id)
+ ok, res = pcall(f, body, id)
+ if ok then
+ body = res
+ end
end
header[#header + 1] = ""
diff --git a/lua/feed/ui/format.lua b/lua/feed/ui/format.lua
index dc1a406..5103c40 100644
--- a/lua/feed/ui/format.lua
+++ b/lua/feed/ui/format.lua
@@ -31,15 +31,7 @@ function M.tags(id, db)
end
end
- local len = Config.layout.tags.width - 2
-
- -- for _, v in ipairs(Config.layout) do
- -- if v[1] == "tags" then
- -- len = v.width - 2
- -- end
- -- end
-
- return "[" .. ut.align(table.concat(acc, ", "), len) .. "]"
+ return "[" .. ut.align(table.concat(acc, ", "), Config.layout.tags.width - 2) .. "]"
end
---@param id string
@@ -65,17 +57,15 @@ end
---@param id string
---@param db feed.db
---@return string
-M.author = function(id)
+M.author = function(id, db)
db = db or require("feed.db")
---@type feed.entry
local entry = db[id]
- local text
- if entry.author == "" then
- text = entry.feed
+ if entry.author then
+ return cleanup(entry.author)
else
- text = entry.author
+ return M.feed(id, db)
end
- return cleanup(text)
end
---@param id string
diff --git a/lua/feed/ui/progress.lua b/lua/feed/ui/progress.lua
index 21ec9cf..2581e4b 100644
--- a/lua/feed/ui/progress.lua
+++ b/lua/feed/ui/progress.lua
@@ -7,6 +7,11 @@ local function format_message(idx, total, message)
return ("[%d/%d] %s"):format(idx, total, message)
end
+---@class feed.progress
+---@field total integer
+---@field count integer
+---@field t integer
+---@field update fun(self: feed.progress, message: string)
local M = {}
M.__index = M
diff --git a/lua/feed/utils.lua b/lua/feed/utils.lua
index 4c7899c..c5306bf 100644
--- a/lua/feed/utils.lua
+++ b/lua/feed/utils.lua
@@ -5,4 +5,11 @@ M = vim.tbl_extend("keep", M, require("feed.utils.url"))
M = vim.tbl_extend("keep", M, require("feed.utils.treesitter"))
M = vim.tbl_extend("keep", M, require("feed.utils.strings"))
+M.decode = function(str)
+ if not str then
+ return nil
+ end
+ return require("feed.lib.entities").decode(str)
+end
+
return M
diff --git a/lua/feed/utils/shared.lua b/lua/feed/utils/shared.lua
index 44b0bb1..5159bac 100644
--- a/lua/feed/utils/shared.lua
+++ b/lua/feed/utils/shared.lua
@@ -149,4 +149,31 @@ M.is_headless = function()
return vim.tbl_isempty(api.nvim_list_uis())
end
+---@param thing table | string
+---@param field string | integer
+---@return string
+M.sensible = function(thing, field, fallback)
+ if not thing then
+ return fallback
+ end
+ if type(thing) == "table" then
+ --- TODO: handle if list
+ if vim.tbl_isempty(thing) then
+ return fallback
+ elseif type(thing[field]) == "string" then
+ return thing[field]
+ else
+ return fallback
+ end
+ elseif type(thing) == "string" then
+ if thing == "" then
+ return fallback
+ else
+ return thing
+ end
+ else
+ return fallback or ""
+ end
+end
+
return M
diff --git a/lua/feed/utils/url.lua b/lua/feed/utils/url.lua
index ecf9e26..0d9c111 100644
--- a/lua/feed/utils/url.lua
+++ b/lua/feed/utils/url.lua
@@ -30,8 +30,8 @@ end
--- Returns all URLs in markdown buffer, if any.
---@param src string
----@return string[][]
-M.get_urls = function(src, cur_link)
+---@return string[][]?
+M.get_urls = vim.F.nil_wrap(function(src, cur_link)
local ret = {}
if cur_link then
@@ -72,7 +72,7 @@ M.get_urls = function(src, cur_link)
end
end
return ret
-end
+end)
local function escape_pattern(text)
return "%(" .. text:gsub("([%%%.%[%]%(%)%$%^%+%-%*%?])", "%%%1") .. "%)" -- Escape all magic characters in the pattern
diff --git a/tests/helpers.lua b/tests/helpers.lua
index 9611b56..2b79212 100644
--- a/tests/helpers.lua
+++ b/tests/helpers.lua
@@ -3,11 +3,12 @@ local M = {}
local looks_like_url = require("feed.utils").looks_like_url
local dir = vim.uv.cwd()
-local data_dir = dir .. "/data/"
+local data_dir = dir .. "/data"
function M.readfile(path, prefix)
prefix = prefix or data_dir
- local str = vim.fn.readfile(prefix .. path)
+ local fp = vim.fs.joinpath(prefix, path)
+ local str = vim.fn.readfile(fp)
return table.concat(str)
end
@@ -23,9 +24,9 @@ Expect: not bozo and entries[0]['author_detail']['email'] == 'me@example.co
]]
function M.extract_test(str)
- local case = str:match "not bozo and (%C+)"
+ local case = str:match("not bozo and (%C+)")
if case:sub(1, 4) == "feed" then
- local k, v = case:match "feed%['(%w+)'%] == '(.+)'"
+ local k, v = case:match("feed%['(%w+)'%] == '(.+)'")
return { [k] = v }
elseif case:sub(1, 7) == "entries" then
-- local k, v = case:match "entries%[0%]%['(%w+)'%] == '(.+)'"
diff --git a/tests/test_date.lua b/tests/test_date.lua
index 6e4b297..cfc32ca 100644
--- a/tests/test_date.lua
+++ b/tests/test_date.lua
@@ -39,20 +39,26 @@ T["parse"]["W3CDTF"] = function()
end
T["relative time"]["years ago"] = function()
- local res = M._years_ago(5, { year = 2025, month = 1, day = 2 })
- local expected = os.time { year = 2020, month = 1, day = 2 }
+ local res = M._years_ago(5, os.time({ year = 2025, month = 1, day = 2 }))
+ local expected = os.time({ year = 2020, month = 1, day = 2 })
+ date_eq(expected, res)
+end
+
+T["relative time"]["week ago"] = function()
+ local res = M._weeks_ago(2, os.time({ year = 2025, month = 1, day = 13 }))
+ local expected = os.time({ year = 2024, month = 12, day = 30 })
date_eq(expected, res)
end
T["relative time"]["months ago"] = function()
- local res = M._months_ago(5, { year = 2025, month = 1, day = 2 })
- local expected = os.time { year = 2024, month = 8, day = 2 }
+ local res = M._months_ago(5, os.time({ year = 2025, month = 1, day = 2 }))
+ local expected = os.time({ year = 2024, month = 8, day = 2 })
date_eq(expected, res)
end
T["relative time"]["days ago"] = function()
- local res = M._days_ago(5, { year = 2025, month = 1, day = 2 })
- local expected = os.time { year = 2024, month = 12, day = 28 }
+ local res = M._days_ago(5, os.time({ year = 2025, month = 1, day = 2 }))
+ local expected = os.time({ year = 2024, month = 12, day = 28 })
date_eq(expected, res)
end
diff --git a/tests/test_db.lua b/tests/test_db.lua
index 1851112..1ffa4e9 100644
--- a/tests/test_db.lua
+++ b/tests/test_db.lua
@@ -160,9 +160,9 @@ end
T["filter"]["filter by date"] = function()
simulate_db({
- [1] = { time = date.literal("6-days-ago") },
- [2] = { time = date.literal("7-days-ago") },
- [3] = { time = date.literal("1-day-ago") },
+ [1] = { time = date._days_ago(6) },
+ [2] = { time = date._days_ago(7) },
+ [3] = { time = date._days_ago(1) },
[4] = { time = os.time() },
})
eq({ sha("4"), sha("3") }, db:filter("@5-days-ago"))
diff --git a/tests/test_feedparser.lua b/tests/test_feedparser.lua
index d852e4b..c0e497d 100644
--- a/tests/test_feedparser.lua
+++ b/tests/test_feedparser.lua
@@ -19,7 +19,6 @@ end
local check_feed = function(ast)
is_string(ast.title)
- is_string(ast.desc)
is_url(ast.link)
is_table(ast.entries)
for _, v in ipairs(ast.entries) do
@@ -29,7 +28,6 @@ local check_feed = function(ast)
is_url(v.link)
is_number(v.time)
is_string(v.title)
- is_string(v.author)
is_string(v.feed)
end
end
@@ -149,9 +147,120 @@ T["url resolover"] = MiniTest.new_set({
},
})
-local function check(filename, checks)
+-- https://sample-feeds.rowanmanning.com/
+T["sample-feeds.com"] = MiniTest.new_set({
+ parametrize = {
+ {
+ "sample/apple_podcast.xml",
+ {
+ link = "https://www.apple.com/itunes/podcasts/",
+ author = "The Sunset Explorers",
+ -- desc = [[Love to get outdoors and discover nature's treasures? Hiking Treks is the
+ -- show for you. We review hikes and excursions, review outdoor gear and interview
+ -- a variety of naturalists and adventurers. Look for new episodes each week.]],
+ [1] = {
+ link = "http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3",
+ },
+ },
+ },
+ {
+ "sample/atom.xml",
+ {
+ title = "Example Feed",
+ link = "http://example.org/",
+ author = "John Doe",
+ [1] = {
+ link = "http://example.org/2003/12/13/atom03",
+ title = "Atom-Powered Robots Run Amok",
+ content = "Some text.",
+ },
+ },
+ },
+ {
+ "sample/feedforall.xml",
+ {
+ title = "FeedForAll Sample Feed",
+ desc = "RSS is a fascinating technology. The uses for RSS are expanding daily. Take a closer look at how various industries are using the benefits of RSS in their businesses.",
+ link = "http://www.feedforall.com/industry-solutions.htm",
+ [1] = {
+ title = "RSS Solutions for Restaurants",
+ link = "http://www.feedforall.com/restaurant.htm",
+ },
+ },
+ },
+ -- {
+ -- "sample/youtube.xml",
+ -- {
+ -- title = "Critical Role",
+ -- author = "Critical Role",
+ -- link = "https://www.youtube.com/channel/UCpXBGqwsBkpvcYjsJBQ7LEQ",
+ -- [1] = {
+ -- title = "The Aurora Grows | Critical Role | Campaign 3, Episode 49",
+ -- linke = "https://www.youtube.com/watch?v=0_NVdZp8haA",
+ -- content = [[
+ -- This episode is sponsored by Thorum. Enjoy 20% off your Thorum ring with code Criticalrole at https://Thorum.com
+ --
+ -- Bells Hells travel the aurora-filled skies of the Hellcatch Valley, concocting plans and gathering allies as the days tick down to the apogee solstice...
+ --
+ -- CAPTION STATUS: CAPTIONED BY OUR EDITORS. The closed captions featured on this episode have been curated by our CR editors. For more information on the captioning process, check out: https://critrole.com/cr-transcript-closed-captions-update
+ --
+ -- Due to the improv nature of Critical Role and other RPG content on our channels, some themes and situations that occur in-game may be difficult for some to handle. If certain episodes or scenes become uncomfortable, we strongly suggest taking a break or skipping that particular episode.
+ -- Your health and well-being is important to us and Psycom has a great list of international mental health resources, in case it’s useful: http://bit.ly/PsycomResources
+ --
+ -- Watch Critical Role Campaign 3 live Thursdays at 7pm PT on https://twitch.tv/criticalrole and https://youtube.com/criticalrole. To join our live and moderated community chat, watch the broadcast on our Twitch channel.
+ --
+ -- Twitch subscribers gain instant access to VODs of our shows like Critical Role, Exandria Unlimited, and 4-Sided Dive. But don't worry: Twitch broadcasts will be uploaded to YouTube about 36 hours after airing live, with audio-only podcast versions of select shows on Spotify, Apple Podcasts & Google Podcasts following a week after the initial air date. Twitch subscribers also gain access to our official custom emote set and subscriber badges and the ability to post links in Twitch chat!
+ --
+ -- "It's Thursday Night (Critical Role Theme Song)" by Peter Habib and Sam Riegel
+ -- Original Music by Omar Fadel and Hexany Audio
+ -- "Welcome to Marquet" Art Theme by Colm McGuinness
+ -- Additional Music by Universal Production Music, Epidemic Sounds, and 5 Alarm
+ -- Character Art by Hannah Friederichs
+ --
+ --
+ -- Follow us!
+ -- Website: https://www.critrole.com
+ -- Newsletter: https://critrole.com/newsletter
+ -- Facebook: https://www.facebook.com/criticalrole
+ -- Twitter: https://twitter.com/criticalrole
+ -- Instagram: https://instagram.com/critical_role
+ -- Twitch: https://www.twitch.tv/criticalrole
+ --
+ -- Shops:
+ -- US: https://shop.critrole.com
+ -- UK: https://shop.critrole.co.uk
+ -- EU: https://shop.critrole.eu
+ -- AU: https://shop.critrole.com.au
+ -- CA: https://canada.critrole.com
+ --
+ -- Follow Critical Role Foundation!
+ -- Learn More & Donate: https://criticalrolefoundation.org
+ -- Twitter: https://twitter.com/CriticalRoleFDN
+ -- Facebook: https://facebook.com/CriticalRoleFDN
+ --
+ -- Want games? Follow Darrington Press
+ -- Newsletter: https://darringtonpress.com/newsletter
+ -- Twitter: https://twitter.com/DarringtonPress
+ -- Facebook: https://www.facebook.com/darringtonpress
+ --
+ -- Check out our animated series!
+ -- The Legend of Vox Machina is available now on Prime Video! Watch: https://amzn.to/3o4nBS5
+ -- Listen to The Legend of Vox Machina's official soundtrack here: https://lnk.to/voxmachina
+ --
+ -- #CriticalRole #BellsHells #DungeonsAndDragons
+ -- ]],
+ -- },
+ -- },
+ -- },
+ },
+})
+
+local function check(filename, checks, debug)
local f = M.parse(readfile(filename), "http://placehoder.feed")
assert(f)
+ if debug then
+ vim.print(f)
+ end
for k, v in pairs(checks) do
if type(v) == "table" then
for kk, vv in pairs(v) do
@@ -173,6 +282,7 @@ T["rss"]["works"] = check
T["atom"]["works"] = check
T["json"]["works"] = check
T["url resolover"]["works"] = check
+T["sample-feeds.com"]["works"] = check
--
--- TODO: parse the condition in the feed parser test suite, into a check table, and wemo check!!
diff --git a/tests/test_opml.lua b/tests/test_opml.lua
index a5dd299..1b4222e 100644
--- a/tests/test_opml.lua
+++ b/tests/test_opml.lua
@@ -14,6 +14,7 @@ T["import"]["simple opml"] = function()