Micronaut RSS

Eases the creation of RSS feeds with Micronaut

Version: 5.2.0

1 Introduction

Micronaut RSS eases the creation of RSS feeds with Micronaut.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 What's New?

Micronaut RSS 2.0 uses Micronaut 2.0.

A new module, rss-core, contains core RSS related file. rss module depends on rss-core and HTTP related classes such as FeedController.

4 Breaking Changes

4.1 Breaking Changes in Micronaut RSS 3.0.0

  • Classes, constructors, etc that have been deprecated in previous versions of Micronaut RSS have been removed.

  • RssFeedProvider returns a Publisher

4.2 Breaking Changes in Micronaut RSS 2.0.0

:micronaut-itunespodcast depends only on micronaut-rss-core. You may want to use it in combination with micronaut-rss if you want to use the FeedController.

The artifacts' maven group has been changed to micronaut-rss. The next table shows the maven coordinates changes:

Old

New

io.micronaut.configuration:micronaut-rss

io.micronaut.rss:micronaut-rss

io.micronaut.configuration:micronaut-itunespodcast

io.micronaut.rss:micronaut-itunespodcast

io.micronaut.rss:rss-core

io.micronaut.rss:rss-core

5 RSS 2.0

implementation("io.micronaut.rss:micronaut-rss")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-rss</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.rss:micronaut-rss",
]

This modules exposes a controller FeedController which exposes feeds as described by RSS 2.0 Specification.

The following table displays several characteristics which can be configured:

🔗
Table 1. Configuration Properties for FeedControllerConfigurationProperties
Property Type Description Default value

micronaut.rss.enabled

boolean

Whether FeedController should be enabled. Default value (true).

micronaut.rss.path

java.lang.String

Configures FeedController path. Default value "/feed"

FeedController requires two beans to be present RssFeedProvider and RssFeedRenderer. While DefaultRssFeedRenderer provides a default implementation for RssFeedRenderer, you will need to provide an implementation for RssFeedProvider.

For example, if you wanted to generate a feed as the one described in the RSS 2.0 specifiation’s sample you could create the next bean:

import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.rss.RssChannel;
import io.micronaut.rss.RssFeedProvider;
import io.micronaut.rss.RssItem;
import io.micronaut.rss.language.RssLanguage;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

@Singleton
class MockRssFeedProvider implements RssFeedProvider {
    RssChannel rssChannel = RssChannel.builder("Liftoff News", "http://liftoff.msfc.nasa.gov/", "Liftoff to Space Exploration.")
            .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 4, 0, 0), ZoneId.of("GMT")))
            .lastBuildDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 9, 41, 1), ZoneId.of("GMT")))
            .docs("http://blogs.law.harvard.edu/tech/rss")
            .generator("Weblog Editor 2.0")
            .managingEditor("editor@example.com")
            .webMaster("webmaster@example.com")
            .item(RssItem.builder()
                .title("Star City")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp")
                .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 &lt;a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\"&gt;Star City&lt;/a&gt;.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 3, 9, 39, 21), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573")
                .build())
            .item(RssItem.builder()
                .description("Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 30, 11, 6, 42), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/30.html#item572")
                .build())
            .item(RssItem.builder()
                .title("The Engine That Does More")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 27, 8, 37, 32), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/27.html#item571")
                .build())
            .item(RssItem.builder()
                .title("Astronauts' Dirty Laundry")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 20, 8, 56, 2), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/20.html#item570")
                .build())
            .build();

    @Override
    @SingleResult
    public Publisher<RssChannel> fetch() {
        return Publishers.just(rssChannel);
    }

    @Override
    @SingleResult
    public Publisher<RssChannel> fetchById(Serializable id) {
        if (id != null && id.equals("1")) {
            return Publishers.just(rssChannel);
        }
        return Publishers.empty();
    }
}
from java.io import Serializable
from java.time import LocalDateTime, ZonedDateTime, ZoneId
from jakarta.inject import Singleton
from micronaut.core.async_.annotation import SingleResult
from micronaut.core.async_.publisher import Publishers
from micronaut.rss import RssChannel, RssFeedProvider, RssItem
from micronaut.rss.language import RssLanguage
from org.reactivestreams import Publisher

@Singleton
class MockRssFeedProvider(RssFeedProvider):

    def __init__(self):
        self.rss_channel = (RssChannel.builder("Liftoff News", "http://liftoff.msfc.nasa.gov/", "Liftoff to Space Exploration.")
            .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 4, 0, 0), ZoneId.of("GMT")))
            .lastBuildDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 9, 41, 1), ZoneId.of("GMT")))
            .docs("http://blogs.law.harvard.edu/tech/rss")
            .generator("Weblog Editor 2.0")
            .managingEditor("editor@example.com")
            .webMaster("webmaster@example.com")
            .item(RssItem.builder()
                .title("Star City")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp")
                .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 &lt;a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\"&gt;Star City&lt;/a&gt;.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 3, 9, 39, 21), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573")
                .build())
            .item(RssItem.builder()
                .description("Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 30, 11, 6, 42), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/30.html#item572")
                .build())
            .item(RssItem.builder()
                .title("The Engine That Does More")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 27, 8, 37, 32), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/27.html#item571")
                .build())
            .item(RssItem.builder()
                .title("Astronauts' Dirty Laundry")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 20, 8, 56, 2), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/20.html#item570")
                .build())
            .build())

    @SingleResult
    def fetch(self) -> Publisher[RssChannel]:
        return Publishers.just(self.rss_channel)

    @SingleResult
    def fetchById(self, id: Serializable | None) -> Publisher[RssChannel]:
        if id is not None and id == "1":
            return Publishers.just(self.rss_channel)
        return Publishers.empty()
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.rss.RssChannel
import io.micronaut.rss.RssFeedProvider
import io.micronaut.rss.RssItem
import io.micronaut.rss.language.RssLanguage
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import java.io.Serializable
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime

@Singleton
class MockRssFeedProvider : RssFeedProvider {
    val rssChannel: RssChannel = RssChannel.builder("Liftoff News", "http://liftoff.msfc.nasa.gov/", "Liftoff to Space Exploration.")
        .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
        .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 4, 0, 0), ZoneId.of("GMT")))
        .lastBuildDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 9, 41, 1), ZoneId.of("GMT")))
        .docs("http://blogs.law.harvard.edu/tech/rss")
        .generator("Weblog Editor 2.0")
        .managingEditor("editor@example.com")
        .webMaster("webmaster@example.com")
        .item(RssItem.builder()
            .title("Star City")
            .link("http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp")
            .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 &lt;a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\"&gt;Star City&lt;/a&gt;.")
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 3, 9, 39, 21), ZoneId.of("GMT")))
            .guid("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573")
            .build())
        .item(RssItem.builder()
            .description("Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.")
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 30, 11, 6, 42), ZoneId.of("GMT")))
            .guid("http://liftoff.msfc.nasa.gov/2003/05/30.html#item572")
            .build())
        .item(RssItem.builder()
            .title("The Engine That Does More")
            .link("http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp")
            .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.")
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 27, 8, 37, 32), ZoneId.of("GMT")))
            .guid("http://liftoff.msfc.nasa.gov/2003/05/27.html#item571")
            .build())
        .item(RssItem.builder()
            .title("Astronauts' Dirty Laundry")
            .link("http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp")
            .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.")
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 20, 8, 56, 2), ZoneId.of("GMT")))
            .guid("http://liftoff.msfc.nasa.gov/2003/05/20.html#item570")
            .build())
        .build()

    @SingleResult
    override fun fetch(): Publisher<RssChannel> = Publishers.just(rssChannel)

    @SingleResult
    override fun fetchById(id: Serializable?): Publisher<RssChannel> {
        if (id != null && id == "1") {
            return Publishers.just(rssChannel)
        }
        return Publishers.empty()
    }
}
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.rss.RssChannel
import io.micronaut.rss.RssFeedProvider
import io.micronaut.rss.RssItem
import io.micronaut.rss.language.RssLanguage
import jakarta.inject.Singleton
import org.reactivestreams.Publisher

import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime

@Singleton
class MockRssFeedProvider implements RssFeedProvider {
    RssChannel rssChannel = RssChannel.builder("Liftoff News", "http://liftoff.msfc.nasa.gov/", "Liftoff to Space Exploration.")
            .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
            .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 4, 0, 0), ZoneId.of("GMT")))
            .lastBuildDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 10, 9, 41, 1), ZoneId.of("GMT")))
            .docs("http://blogs.law.harvard.edu/tech/rss")
            .generator("Weblog Editor 2.0")
            .managingEditor("editor@example.com")
            .webMaster("webmaster@example.com")
            .item(RssItem.builder()
                .title("Star City")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp")
                .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 &lt;a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\"&gt;Star City&lt;/a&gt;.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 6, 3, 9, 39, 21), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573")
                .build())
            .item(RssItem.builder()
                .description("Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 30, 11, 6, 42), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/30.html#item572")
                .build())
            .item(RssItem.builder()
                .title("The Engine That Does More")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 27, 8, 37, 32), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/27.html#item571")
                .build())
            .item(RssItem.builder()
                .title("Astronauts' Dirty Laundry")
                .link("http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp")
                .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.")
                .pubDate(ZonedDateTime.of(LocalDateTime.of(2003, 5, 20, 8, 56, 2), ZoneId.of("GMT")))
                .guid("http://liftoff.msfc.nasa.gov/2003/05/20.html#item570")
                .build())
            .build()

    @Override
    @SingleResult
    Publisher<RssChannel> fetch() {
        Publishers.just(rssChannel)
    }

    @Override
    @SingleResult
    Publisher<RssChannel> fetchById(Serializable id) {
        if (id != null && id == "1") {
            return Publishers.just(rssChannel)
        }
        Publishers.empty()
    }
}

6 Itunes Podcast RSS Feed

implementation("io.micronaut.rss:micronaut-itunespodcast")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-itunespodcast</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.rss:micronaut-itunespodcast",
]

An Itunes podcast feed, is a valid RSS 2.0 feed with additional tags.

For example, the following bean will generate the Itunes Podcast RSS Feed’s sample

import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.rss.RssChannel;
import io.micronaut.rss.RssChannelImage;
import io.micronaut.rss.RssFeedProvider;
import io.micronaut.rss.RssItemEnclosure;
import io.micronaut.rss.itunespodcast.ItunesPodcast;
import io.micronaut.rss.itunespodcast.ItunesPodcastCategory;
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisode;
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisodeType;
import io.micronaut.rss.itunespodcast.ItunesPodcastOwner;
import io.micronaut.rss.itunespodcast.ItunesPodcastType;
import io.micronaut.rss.language.RssLanguage;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;

@Singleton
public class ItunesPodcastFeedProvider implements RssFeedProvider {

    @Override
    @SingleResult
    public Publisher<RssChannel> fetch() {
        return Publishers.just(ItunesPodcast.builder()
                .title("Hiking Treks")
                .link("https://www.apple.com/itunes/podcasts/")
                .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
                .copyright("&#8471; &amp; &#xA9; 2017 John Appleseed")
                .description("Love to get outdoors and discover nature&apos;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.")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .author("The Sunset Explorers")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .type(ItunesPodcastType.SERIAL)
                .owner(ItunesPodcastOwner.builder()
                        .name("Sunset Explorers")
                        .email("mountainscape@icloud.com")
                        .build())
                .image(RssChannelImage
                        .builder("Hiking Treks", "http://podcasts.apple.com/resources/example/hiking_treks/images/cover_art.jpg", "https://www.apple.com/itunes/podcasts/")
                        .build())
                .summary("Love to get outdoors and discover nature&apos;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.")
                .category(Arrays.asList(ItunesPodcastCategory.SPORTS_AND_RECREATION_OUTDOOR.getCategories()))
                .explicit(false)
                .item(ItunesPodcastEpisode.builder("Hiking Treks Trailer")
                        .episodeType(ItunesPodcastEpisodeType.TRAILER)
                        .author("The Sunset Adventurers")
                        .subtitle("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .summary("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .description("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .contentEncoded("<![CDATA[The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>]]>")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 4, 12, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("17:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Mt. Hood, Oregon")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(2)
                        .title("S02 EP04 Mt. Hood, Oregon")
                        .author("The Sunset Explorers")
                        .subtitle("Tips for trekking around the tallest mountain in Oregon")
                        .summary("Tips for trekking around the tallest mountain in Oregon")
                        .description("Tips for trekking around the tallest mountain in Oregon")
                        .contentEncoded("Tips for trekking around the tallest mountain in Oregon")
                        .enclosure(RssItemEnclosure.builder()
                                .length(8727310)
                                .type("audio/x-m4a")
                                .url("http://example.com/podcasts/everything/mthood.m4a")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170606.m4a")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 6, 6, 12, 0, 0), ZoneId.of("GMT")))
                        .duration("17:04")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Bouldering Around Boulder")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(2)
                        .title("S02 EP03 Bouldering Around Boulder")
                        .author("The Sunset Adventurers")
                        .subtitle("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .summary("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .description("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("video/mp4")
                                .url("http://example.com/podcasts/boulder.mp4")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170530.mp4")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 30, 13, 0, 0), ZoneId.of("GMT")))
                        .duration("06:27")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Caribou Mountain, Maine")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(2)
                        .title("S02 EP02 Caribou Mountain, Maine")
                        .author("The Sunset Adventurers")
                        .subtitle("Put your fitness to the test with this invigorating hill climb.")
                        .summary("Put your fitness to the test with this invigorating hill climb.")
                        .description("Put your fitness to the test with this invigorating hill climb.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("audio/x-m4v")
                                .url("http://example.com/podcasts/everything/caribou.m4v")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170523.m4v")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 23, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("04:34")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Stawamus Chief")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(2)
                        .title("S02 EP01 Stawamus Chief")
                        .author("The Sunset Adventurers")
                        .subtitle("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .summary("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .description("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .contentEncoded("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170516.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 16, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("13:24")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Kuliouou Ridge Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(1)
                        .title("S01 EP04 Kuliouou Ridge Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .summary("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .description("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .contentEncoded("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160509.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 10, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("15:29")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Blood Mountain Loop")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(1)
                        .title("S01 EP03 Blood Mountain Loop")
                        .author("The Sunset Adventurers")
                        .subtitle("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .summary("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .description("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .contentEncoded("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160502.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 3, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("24:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Garden of the Gods Wilderness")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(1)
                        .title("S01 EP02 Garden of the Gods Wilderness")
                        .author("The Sunset Adventurers")
                        .subtitle("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .summary("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .description("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .contentEncoded("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160425.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 26, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("13:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Upper Priest Lake Trail to Continental Creek Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(1)
                        .title("S01 EP01 Upper Priest Lake Trail to Continental Creek Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .summary("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .description("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .contentEncoded("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418a.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 19, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("23:59")
                        .explicit(false)
                        .contentEncoded("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>")
                        .build()).build());
    }

    @Override
    @SingleResult
    public Publisher<RssChannel> fetchById(Serializable id) {
        return Publishers.empty();
    }
}
from java.io import Serializable
from java.time import LocalDateTime, ZonedDateTime, ZoneId
from jakarta.inject import Singleton
from micronaut.core.async_.annotation import SingleResult
from micronaut.core.async_.publisher import Publishers
from micronaut.rss import RssChannel, RssChannelImage, RssFeedProvider, RssItemEnclosure
from micronaut.rss.itunespodcast import (
    ItunesPodcast,
    ItunesPodcastCategory,
    ItunesPodcastEpisode,
    ItunesPodcastEpisodeType,
    ItunesPodcastOwner,
    ItunesPodcastType,
)
from micronaut.rss.language import RssLanguage
from org.reactivestreams import Publisher

@Singleton
class ItunesPodcastFeedProvider(RssFeedProvider):

    @SingleResult
    def fetch(self) -> Publisher[RssChannel]:
        return Publishers.just(ItunesPodcast.builder()
                .title("Hiking Treks")
                .link("https://www.apple.com/itunes/podcasts/")
                .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
                .copyright("&#8471; &amp; &#xA9; 2017 John Appleseed")
                .description("Love to get outdoors and discover nature&apos;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.")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .author("The Sunset Explorers")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .type(ItunesPodcastType.SERIAL)
                .owner(ItunesPodcastOwner.builder()
                        .name("Sunset Explorers")
                        .email("mountainscape@icloud.com")
                        .build())
                .image(RssChannelImage
                        .builder("Hiking Treks", "http://podcasts.apple.com/resources/example/hiking_treks/images/cover_art.jpg", "https://www.apple.com/itunes/podcasts/")
                        .build())
                .summary("Love to get outdoors and discover nature&apos;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.")
                .category([ItunesPodcastCategory.SPORTS_AND_RECREATION_OUTDOOR.getCategories()])
                .explicit(False)
                .item(ItunesPodcastEpisode.builder("Hiking Treks Trailer")
                        .episodeType(ItunesPodcastEpisodeType.TRAILER)
                        .author("The Sunset Adventurers")
                        .subtitle("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .summary("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .description("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .contentEncoded("<![CDATA[The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>]]>")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 4, 12, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("17:59")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Mt. Hood, Oregon")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(2)
                        .title("S02 EP04 Mt. Hood, Oregon")
                        .author("The Sunset Explorers")
                        .subtitle("Tips for trekking around the tallest mountain in Oregon")
                        .summary("Tips for trekking around the tallest mountain in Oregon")
                        .description("Tips for trekking around the tallest mountain in Oregon")
                        .contentEncoded("Tips for trekking around the tallest mountain in Oregon")
                        .enclosure(RssItemEnclosure.builder()
                                .length(8727310)
                                .type("audio/x-m4a")
                                .url("http://example.com/podcasts/everything/mthood.m4a")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170606.m4a")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 6, 6, 12, 0, 0), ZoneId.of("GMT")))
                        .duration("17:04")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Bouldering Around Boulder")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(2)
                        .title("S02 EP03 Bouldering Around Boulder")
                        .author("The Sunset Adventurers")
                        .subtitle("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .summary("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .description("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("video/mp4")
                                .url("http://example.com/podcasts/boulder.mp4")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170530.mp4")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 30, 13, 0, 0), ZoneId.of("GMT")))
                        .duration("06:27")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Caribou Mountain, Maine")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(2)
                        .title("S02 EP02 Caribou Mountain, Maine")
                        .author("The Sunset Adventurers")
                        .subtitle("Put your fitness to the test with this invigorating hill climb.")
                        .summary("Put your fitness to the test with this invigorating hill climb.")
                        .description("Put your fitness to the test with this invigorating hill climb.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("audio/x-m4v")
                                .url("http://example.com/podcasts/everything/caribou.m4v")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170523.m4v")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 23, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("04:34")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Stawamus Chief")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(2)
                        .title("S02 EP01 Stawamus Chief")
                        .author("The Sunset Adventurers")
                        .subtitle("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .summary("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .description("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .contentEncoded("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170516.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 16, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("13:24")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Kuliouou Ridge Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(1)
                        .title("S01 EP04 Kuliouou Ridge Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .summary("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .description("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .contentEncoded("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160509.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 10, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("15:29")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Blood Mountain Loop")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(1)
                        .title("S01 EP03 Blood Mountain Loop")
                        .author("The Sunset Adventurers")
                        .subtitle("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .summary("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .description("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .contentEncoded("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160502.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 3, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("24:59")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Garden of the Gods Wilderness")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(1)
                        .title("S01 EP02 Garden of the Gods Wilderness")
                        .author("The Sunset Adventurers")
                        .subtitle("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .summary("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .description("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .contentEncoded("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160425.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 26, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("13:59")
                        .explicit(False)
                        .build())
                .item(ItunesPodcastEpisode.builder("Upper Priest Lake Trail to Continental Creek Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(1)
                        .title("S01 EP01 Upper Priest Lake Trail to Continental Creek Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .summary("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .description("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .contentEncoded("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418a.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 19, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("23:59")
                        .explicit(False)
                        .contentEncoded("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>")
                        .build()).build())

    @SingleResult
    def fetchById(self, id: Serializable | None) -> Publisher[RssChannel]:
        return Publishers.empty()
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.rss.RssChannel
import io.micronaut.rss.RssChannelImage
import io.micronaut.rss.RssFeedProvider
import io.micronaut.rss.RssItemEnclosure
import io.micronaut.rss.itunespodcast.ItunesPodcast
import io.micronaut.rss.itunespodcast.ItunesPodcastCategory
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisode
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisodeType
import io.micronaut.rss.itunespodcast.ItunesPodcastOwner
import io.micronaut.rss.itunespodcast.ItunesPodcastType
import io.micronaut.rss.language.RssLanguage
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import java.io.Serializable
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime

@Singleton
class ItunesPodcastFeedProvider : RssFeedProvider {

    @SingleResult
    override fun fetch(): Publisher<RssChannel> {
        return Publishers.just(ItunesPodcast.builder()
                .title("Hiking Treks")
                .link("https://www.apple.com/itunes/podcasts/")
                .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
                .copyright("&#8471; &amp; &#xA9; 2017 John Appleseed")
                .description("Love to get outdoors and discover nature&apos;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.")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .author("The Sunset Explorers")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .type(ItunesPodcastType.SERIAL)
                .owner(ItunesPodcastOwner.builder()
                        .name("Sunset Explorers")
                        .email("mountainscape@icloud.com")
                        .build())
                .image(RssChannelImage
                        .builder("Hiking Treks", "http://podcasts.apple.com/resources/example/hiking_treks/images/cover_art.jpg", "https://www.apple.com/itunes/podcasts/")
                        .build())
                .summary("Love to get outdoors and discover nature&apos;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.")
                .category(listOf(ItunesPodcastCategory.SPORTS_AND_RECREATION_OUTDOOR.categories))
                .explicit(false)
                .item(ItunesPodcastEpisode.builder("Hiking Treks Trailer")
                        .episodeType(ItunesPodcastEpisodeType.TRAILER)
                        .author("The Sunset Adventurers")
                        .subtitle("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .summary("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .description("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .contentEncoded("<![CDATA[The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>]]>")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 4, 12, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("17:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Mt. Hood, Oregon")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(2)
                        .title("S02 EP04 Mt. Hood, Oregon")
                        .author("The Sunset Explorers")
                        .subtitle("Tips for trekking around the tallest mountain in Oregon")
                        .summary("Tips for trekking around the tallest mountain in Oregon")
                        .description("Tips for trekking around the tallest mountain in Oregon")
                        .contentEncoded("Tips for trekking around the tallest mountain in Oregon")
                        .enclosure(RssItemEnclosure.builder()
                                .length(8727310)
                                .type("audio/x-m4a")
                                .url("http://example.com/podcasts/everything/mthood.m4a")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170606.m4a")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 6, 6, 12, 0, 0), ZoneId.of("GMT")))
                        .duration("17:04")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Bouldering Around Boulder")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(2)
                        .title("S02 EP03 Bouldering Around Boulder")
                        .author("The Sunset Adventurers")
                        .subtitle("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .summary("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .description("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("video/mp4")
                                .url("http://example.com/podcasts/boulder.mp4")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170530.mp4")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 30, 13, 0, 0), ZoneId.of("GMT")))
                        .duration("06:27")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Caribou Mountain, Maine")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(2)
                        .title("S02 EP02 Caribou Mountain, Maine")
                        .author("The Sunset Adventurers")
                        .subtitle("Put your fitness to the test with this invigorating hill climb.")
                        .summary("Put your fitness to the test with this invigorating hill climb.")
                        .description("Put your fitness to the test with this invigorating hill climb.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("audio/x-m4v")
                                .url("http://example.com/podcasts/everything/caribou.m4v")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170523.m4v")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 23, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("04:34")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Stawamus Chief")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(2)
                        .title("S02 EP01 Stawamus Chief")
                        .author("The Sunset Adventurers")
                        .subtitle("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .summary("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .description("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .contentEncoded("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170516.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 16, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("13:24")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Kuliouou Ridge Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(1)
                        .title("S01 EP04 Kuliouou Ridge Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .summary("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .description("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .contentEncoded("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160509.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 10, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("15:29")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Blood Mountain Loop")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(1)
                        .title("S01 EP03 Blood Mountain Loop")
                        .author("The Sunset Adventurers")
                        .subtitle("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .summary("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .description("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .contentEncoded("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160502.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 3, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("24:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Garden of the Gods Wilderness")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(1)
                        .title("S01 EP02 Garden of the Gods Wilderness")
                        .author("The Sunset Adventurers")
                        .subtitle("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .summary("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .description("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .contentEncoded("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160425.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 26, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("13:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Upper Priest Lake Trail to Continental Creek Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(1)
                        .title("S01 EP01 Upper Priest Lake Trail to Continental Creek Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .summary("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .description("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .contentEncoded("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418a.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 19, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("23:59")
                        .explicit(false)
                        .contentEncoded("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>")
                        .build()).build())
    }

    @SingleResult
    override fun fetchById(id: Serializable?): Publisher<RssChannel> = Publishers.empty()
}
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.rss.RssChannel
import io.micronaut.rss.RssChannelImage
import io.micronaut.rss.RssFeedProvider
import io.micronaut.rss.RssItemEnclosure
import io.micronaut.rss.itunespodcast.ItunesPodcast
import io.micronaut.rss.itunespodcast.ItunesPodcastCategory
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisode
import io.micronaut.rss.itunespodcast.ItunesPodcastEpisodeType
import io.micronaut.rss.itunespodcast.ItunesPodcastOwner
import io.micronaut.rss.itunespodcast.ItunesPodcastType
import io.micronaut.rss.language.RssLanguage
import jakarta.inject.Singleton
import org.reactivestreams.Publisher

import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime

@Singleton
class ItunesPodcastFeedProvider implements RssFeedProvider {

    @Override
    @SingleResult
    Publisher<RssChannel> fetch() {
        return Publishers.just(ItunesPodcast.builder()
                .title("Hiking Treks")
                .link("https://www.apple.com/itunes/podcasts/")
                .language(RssLanguage.LANG_ENGLISH_UNITED_STATES)
                .copyright("&#8471; &amp; &#xA9; 2017 John Appleseed")
                .description("Love to get outdoors and discover nature&apos;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.")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .author("The Sunset Explorers")
                .subtitle("Find your trail. Great hikes and outdoor adventures.")
                .type(ItunesPodcastType.SERIAL)
                .owner(ItunesPodcastOwner.builder()
                        .name("Sunset Explorers")
                        .email("mountainscape@icloud.com")
                        .build())
                .image(RssChannelImage
                        .builder("Hiking Treks", "http://podcasts.apple.com/resources/example/hiking_treks/images/cover_art.jpg", "https://www.apple.com/itunes/podcasts/")
                        .build())
                .summary("Love to get outdoors and discover nature&apos;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.")
                .category([ItunesPodcastCategory.SPORTS_AND_RECREATION_OUTDOOR.categories])
                .explicit(false)
                .item(ItunesPodcastEpisode.builder("Hiking Treks Trailer")
                        .episodeType(ItunesPodcastEpisodeType.TRAILER)
                        .author("The Sunset Adventurers")
                        .subtitle("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .summary("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .description("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States.")
                        .contentEncoded("<![CDATA[The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>]]>")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 4, 12, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("17:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Mt. Hood, Oregon")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(2)
                        .title("S02 EP04 Mt. Hood, Oregon")
                        .author("The Sunset Explorers")
                        .subtitle("Tips for trekking around the tallest mountain in Oregon")
                        .summary("Tips for trekking around the tallest mountain in Oregon")
                        .description("Tips for trekking around the tallest mountain in Oregon")
                        .contentEncoded("Tips for trekking around the tallest mountain in Oregon")
                        .enclosure(RssItemEnclosure.builder()
                                .length(8727310)
                                .type("audio/x-m4a")
                                .url("http://example.com/podcasts/everything/mthood.m4a")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170606.m4a")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2016, 6, 6, 12, 0, 0), ZoneId.of("GMT")))
                        .duration("17:04")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Bouldering Around Boulder")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(2)
                        .title("S02 EP03 Bouldering Around Boulder")
                        .author("The Sunset Adventurers")
                        .subtitle("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .summary("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .description("We explore fun walks to climbing areas about the beautiful Colorado city of Boulder.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode2.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("video/mp4")
                                .url("http://example.com/podcasts/boulder.mp4")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170530.mp4")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 30, 13, 0, 0), ZoneId.of("GMT")))
                        .duration("06:27")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Caribou Mountain, Maine")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(2)
                        .title("S02 EP02 Caribou Mountain, Maine")
                        .author("The Sunset Adventurers")
                        .subtitle("Put your fitness to the test with this invigorating hill climb.")
                        .summary("Put your fitness to the test with this invigorating hill climb.")
                        .description("Put your fitness to the test with this invigorating hill climb.")
                        .image("http://example.com/podcasts/everything/AllAboutEverything/Episode3.jpg")
                        .contentEncoded("href=\"http://example.com/podcasts/everything/\"")
                        .enclosure(RssItemEnclosure.builder()
                                .length(5650889)
                                .type("audio/x-m4v")
                                .url("http://example.com/podcasts/everything/caribou.m4v")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170523.m4v")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 23, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("04:34")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Stawamus Chief")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(2)
                        .title("S02 EP01 Stawamus Chief")
                        .author("The Sunset Adventurers")
                        .subtitle("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .summary("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .description("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .contentEncoded("We tackle Stawamus Chief outside of Vancouver, BC and you should too!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20170516.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 16, 2, 0, 0), ZoneId.of("GMT")))
                        .duration("13:24")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Kuliouou Ridge Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(4)
                        .season(1)
                        .title("S01 EP04 Kuliouou Ridge Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .summary("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .description("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .contentEncoded("Oahu, Hawaii, has some picturesque hikes and this is one of the best!")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160509.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 10, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("15:29")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Blood Mountain Loop")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(3)
                        .season(1)
                        .title("S01 EP03 Blood Mountain Loop")
                        .author("The Sunset Adventurers")
                        .subtitle("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .summary("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .description("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .contentEncoded("Hiking the Appalachian Trail and Freeman Trail in Georgia")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160502.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 5, 3, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("24:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Garden of the Gods Wilderness")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(2)
                        .season(1)
                        .title("S01 EP02 Garden of the Gods Wilderness")
                        .author("The Sunset Adventurers")
                        .subtitle("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .summary("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .description("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .contentEncoded("Wilderness Area Garden of the Gods in Illinois is a delightful spot for an extended hike.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160425.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 26, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("13:59")
                        .explicit(false)
                        .build())
                .item(ItunesPodcastEpisode.builder("Upper Priest Lake Trail to Continental Creek Trail")
                        .episodeType(ItunesPodcastEpisodeType.FULL)
                        .episode(1)
                        .season(1)
                        .title("S01 EP01 Upper Priest Lake Trail to Continental Creek Trail")
                        .author("The Sunset Adventurers")
                        .subtitle("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .summary("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .description("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .contentEncoded("We check out this powerfully scenic hike following the river in the Idaho Panhandle National Forests.")
                        .enclosure(RssItemEnclosure.builder()
                                .length(498537)
                                .type("audio/mpeg")
                                .url("http://example.com/podcasts/everything/AllAboutEverythingEpisode4.mp3")
                                .build())
                        .guid("http://example.com/podcasts/archive/aae20160418a.mp3")
                        .pubDate(ZonedDateTime.of(LocalDateTime.of(2017, 4, 19, 1, 15, 0), ZoneId.of("GMT")))
                        .duration("23:59")
                        .explicit(false)
                        .contentEncoded("The Sunset Explorers share tips, techniques and recommendations for great hikes and adventures around the United States. Listen on <a href=\"https://www.apple.com/itunes/podcasts/\">Apple Podcasts</a>")
                        .build()).build())
    }

    @Override
    @SingleResult
    Publisher<RssChannel> fetchById(Serializable id) {
        Publishers.empty()
    }
}

7 JSON Feeds

JSON Feed is a format similar to RSS and Atom but in JSON.

The following dependency contains the core classes to build a JSON Feed.

implementation("io.micronaut.rss:micronaut-jsonfeed-core")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-jsonfeed-core</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.rss:micronaut-jsonfeed-core",
]

To get such JSON:

{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "My Example Feed",
    "home_page_url": "https://example.org/",
    "feed_url": "https://example.org/feed.json",
    "items": [
        {
            "id": "2",
            "content_text": "This is a second item.",
            "url": "https://example.org/second-item"
        },
        {
            "id": "1",
            "content_html": "<p>Hello, world!</p>",
            "url": "https://example.org/initial-post"
       }
    ]
}        

You can write:

import io.micronaut.rss.jsonfeed.JsonFeed;
import io.micronaut.rss.jsonfeed.JsonFeedItem;

JsonFeed feed = JsonFeed.builder()
        .version("https://jsonfeed.org/version/1.1")
        .title("My Example Feed")
        .homePageUrl("https://example.org/")
        .feedUrl("https://example.org/feed.json")
        .item(JsonFeedItem.builder()
                .id("2")
                .contentText("This is a second item.")
                .url("https://example.org/second-item")
                .build())
        .item(JsonFeedItem.builder()
                .id("1")
                .contentHtml("<p>Hello, world!</p>")
                .url("https://example.org/initial-post")
                .build())
        .build();
from micronaut.rss.jsonfeed import JsonFeed, JsonFeedItem

feed = (JsonFeed.builder()
    .version("https://jsonfeed.org/version/1.1")
    .title("My Example Feed")
    .homePageUrl("https://example.org/")
    .feedUrl("https://example.org/feed.json")
    .item(JsonFeedItem.builder()
        .id("2")
        .contentText("This is a second item.")
        .url("https://example.org/second-item")
        .build())
    .item(JsonFeedItem.builder()
        .id("1")
        .contentHtml("<p>Hello, world!</p>")
        .url("https://example.org/initial-post")
        .build())
    .build())
import io.micronaut.rss.jsonfeed.JsonFeed
import io.micronaut.rss.jsonfeed.JsonFeedItem

val feed = JsonFeed.builder()
    .version("https://jsonfeed.org/version/1.1")
    .title("My Example Feed")
    .homePageUrl("https://example.org/")
    .feedUrl("https://example.org/feed.json")
    .item(JsonFeedItem.builder()
        .id("2")
        .contentText("This is a second item.")
        .url("https://example.org/second-item")
        .build())
    .item(JsonFeedItem.builder()
        .id("1")
        .contentHtml("<p>Hello, world!</p>")
        .url("https://example.org/initial-post")
        .build())
    .build()
import io.micronaut.rss.jsonfeed.JsonFeed
import io.micronaut.rss.jsonfeed.JsonFeedItem

JsonFeed feed = JsonFeed.builder()
        .version("https://jsonfeed.org/version/1.1")
        .title("My Example Feed")
        .homePageUrl("https://example.org/")
        .feedUrl("https://example.org/feed.json")
        .item(JsonFeedItem.builder()
                .id("2")
                .contentText("This is a second item.")
                .url("https://example.org/second-item")
                .build())
        .item(JsonFeedItem.builder()
                .id("1")
                .contentHtml("<p>Hello, world!</p>")
                .url("https://example.org/initial-post")
                .build())
        .build()

7.1 JSON Feeds Endpoint

The following dependency contains functionality to expose an endpoint to return a JSON Feed:

implementation("io.micronaut.rss:micronaut-jsonfeed")
<dependency>
    <groupId>io.micronaut.rss</groupId>
    <artifactId>micronaut-jsonfeed</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.rss:micronaut-jsonfeed",
]

Your JSON Feed will be of content type application/json+feed.

You have to register an additional type for Micronaut’s json codec:

micronaut.codec.json.additional-types[0]=application/json+feed

Then, if you provide a bean of type JsonFeedProvider such as:

import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.rss.jsonfeed.JsonFeed;
import io.micronaut.rss.jsonfeed.JsonFeedAuthor;
import io.micronaut.rss.jsonfeed.JsonFeedItem;
import io.micronaut.rss.jsonfeed.http.JsonFeedProvider;
import jakarta.inject.Singleton;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;

@Singleton
public class ExampleJsonFeedProvider implements JsonFeedProvider {

    @NonNull
    @SingleResult
    @Override
    public Publisher<JsonFeed> feed(@Nullable Integer maxNumberOfItems, @Nullable Integer pageNumber) {
        return Flux.create(emitter -> {
            emitter.next(JsonFeed.builder()
                    .version("https://jsonfeed.org/version/1.1")
                    .title("My Example Feed")
                    .homePageUrl("https://example.org/")
                    .feedUrl("https://example.org/feed.json")
                    .author(JsonFeedAuthor.builder()
                            .name("Feed Author")
                            .url("mailto:feed@example.org")
                            .build())
                    .item(JsonFeedItem.builder()
                            .id("2")
                            .contentText("This is a second item.")
                            .author(JsonFeedAuthor.builder()
                                    .name("Item Author")
                                    .build())
                            .url("https://example.org/second-item")
                            .build())
                    .item(JsonFeedItem.builder()
                            .id("1")
                            .contentHtml("<p>Hello, world!</p>")
                            .url("https://example.org/initial-post")
                            .build())
                    .build());
            emitter.complete();
        }, FluxSink.OverflowStrategy.ERROR);
    }
}
from jakarta.inject import Singleton
from micronaut.core.async_.annotation import SingleResult
from micronaut.rss.jsonfeed import JsonFeed, JsonFeedAuthor, JsonFeedItem
from micronaut.rss.jsonfeed.http import JsonFeedProvider
from org.reactivestreams import Publisher
from reactor.core.publisher import Flux, FluxSink

@Singleton
class ExampleJsonFeedProvider(JsonFeedProvider):

    @SingleResult
    def feed(self, max_number_of_items: int | None, page_number: int | None) -> Publisher[JsonFeed]:
        def emit(emitter):
            emitter.next(JsonFeed.builder()
                .version("https://jsonfeed.org/version/1.1")
                .title("My Example Feed")
                .homePageUrl("https://example.org/")
                .feedUrl("https://example.org/feed.json")
                .author(JsonFeedAuthor.builder()
                    .name("Feed Author")
                    .url("mailto:feed@example.org")
                    .build())
                .item(JsonFeedItem.builder()
                    .id("2")
                    .contentText("This is a second item.")
                    .author(JsonFeedAuthor.builder()
                        .name("Item Author")
                        .build())
                    .url("https://example.org/second-item")
                    .build())
                .item(JsonFeedItem.builder()
                    .id("1")
                    .contentHtml("<p>Hello, world!</p>")
                    .url("https://example.org/initial-post")
                    .build())
                .build())
            emitter.complete()

        return Flux.create(emit, FluxSink.OverflowStrategy.ERROR)
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.rss.jsonfeed.JsonFeed
import io.micronaut.rss.jsonfeed.JsonFeedAuthor
import io.micronaut.rss.jsonfeed.JsonFeedItem
import io.micronaut.rss.jsonfeed.http.JsonFeedProvider
import jakarta.inject.Singleton
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.FluxSink

@Singleton
class ExampleJsonFeedProvider : JsonFeedProvider {

    @SingleResult
    override fun feed(maxNumberOfItems: Int?, pageNumber: Int?): Publisher<JsonFeed> {
        return Flux.create({ emitter ->
            emitter.next(JsonFeed.builder()
                .version("https://jsonfeed.org/version/1.1")
                .title("My Example Feed")
                .homePageUrl("https://example.org/")
                .feedUrl("https://example.org/feed.json")
                .author(JsonFeedAuthor.builder()
                    .name("Feed Author")
                    .url("mailto:feed@example.org")
                    .build())
                .item(JsonFeedItem.builder()
                    .id("2")
                    .contentText("This is a second item.")
                    .author(JsonFeedAuthor.builder()
                        .name("Item Author")
                        .build())
                    .url("https://example.org/second-item")
                    .build())
                .item(JsonFeedItem.builder()
                    .id("1")
                    .contentHtml("<p>Hello, world!</p>")
                    .url("https://example.org/initial-post")
                    .build())
                .build())
            emitter.complete()
        }, FluxSink.OverflowStrategy.ERROR)
    }
}
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.rss.jsonfeed.JsonFeed
import io.micronaut.rss.jsonfeed.JsonFeedAuthor
import io.micronaut.rss.jsonfeed.JsonFeedItem
import io.micronaut.rss.jsonfeed.http.JsonFeedProvider
import jakarta.inject.Singleton
import org.jspecify.annotations.NonNull
import org.jspecify.annotations.Nullable
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.FluxSink

@Singleton
class ExampleJsonFeedProvider implements JsonFeedProvider {

    @NonNull
    @SingleResult
    @Override
    Publisher<JsonFeed> feed(@Nullable Integer maxNumberOfItems, @Nullable Integer pageNumber) {
        Flux.create({ emitter ->
            emitter.next(JsonFeed.builder()
                    .version("https://jsonfeed.org/version/1.1")
                    .title("My Example Feed")
                    .homePageUrl("https://example.org/")
                    .feedUrl("https://example.org/feed.json")
                    .author(JsonFeedAuthor.builder()
                            .name("Feed Author")
                            .url("mailto:feed@example.org")
                            .build())
                    .item(JsonFeedItem.builder()
                            .id("2")
                            .contentText("This is a second item.")
                            .author(JsonFeedAuthor.builder()
                                    .name("Item Author")
                                    .build())
                            .url("https://example.org/second-item")
                            .build())
                    .item(JsonFeedItem.builder()
                            .id("1")
                            .contentHtml("<p>Hello, world!</p>")
                            .url("https://example.org/initial-post")
                            .build())
                    .build())
            emitter.complete()
        }, FluxSink.OverflowStrategy.ERROR)
    }
}
If the creation of the JSON feed is a blocking I/O operation, offload that tasks to a separate thread pool that does not block the Event loop.

A GET request to /feeds/json returns a 200 OK response with HTTP Header with name Content-Type with value application/json+feed and a JSON Payload in the body such as:

{
    "version": "https://jsonfeed.org/version/1.1",
    "title": "My Example Feed",
    "home_page_url": "https://example.org/",
    "feed_url": "https://example.org/feed.json",
    "authors": [
        {
            "name": "Feed Author",
            "url": "mailto:feed@example.org"
        }
    ],
    "items": [
        {
            "id": "2",
            "content_text": "This is a second item.",
            "authors": [
                {
                    "name": "Item Author"
                }
            ],
            "url": "https://example.org/second-item"
        },
        {
            "id": "1",
            "content_html": "<p>Hello, world!</p>",
            "url": "https://example.org/initial-post"
       }
    ]
}        

There are additional configuration options for the JSON Feed Controller:

🔗
Table 1. Configuration Properties for JsonFeedControllerConfigurationProperties
Property Type Description Default value

jsonfeed.root-path

java.lang.String

Configures JsonFeedController rootpath. Default value "/feeds"

jsonfeed.path

java.lang.String

Configures JsonFeedController path. Default value "/json"

jsonfeed.enabled

boolean

Whether JsonFeedController should be enabled. Default value (true).

You can implement pagination by supplying maxNumberOfItems and pageNumber which will be passed to your implementation of JsonFeedProvider.

8 Repository

You can find the source code of this project in this repository: