app/com/gu/itunes/iTunesRssFeed.scala (110 lines of code) (raw):

package com.gu.itunes import com.gu.contentapi.client.model.v1._ import org.joda.time.DateTime import org.scalactic.{ Bad, Good, Or } import play.api.mvc.Results._ import java.time.Instant import scala.xml.Node object iTunesRssFeed { val author = "The Guardian" /* Before 24th December 2021 the podcasts feed had 200 items, but with items * 100-200 repeated so it looked like 300 items. Apple seems to have mostly * ignored items >100 in the feed, presumably because of how they handle * duplicates. * * On 24th December 2021 we fixed the bug so that the feed had 300 unique items. * * Apple downloads of any items that were brought back into the feed by this * change increased massively. They have since reduced but not to the level we * would expect. The appleExcessDownloadsWorkaround is intended to allow us to * gradually increase the number of podcasts in the feed to 300, without having * to bring old items back into the feed. * * We should be able to remove this workaround at some point in the future, when * it is no longer filtering out any items. */ private val feedChangeDate = Instant.parse("2021-12-24T10:30:00Z") private def afterFeedChangeDate(capiDateTime: CapiDateTime) = Instant.ofEpochMilli(capiDateTime.dateTime).isAfter(feedChangeDate) private def appleExcessDownloadsWorkaround(items: List[Content]) = { val (afterChange, beforeChange) = items.partition(_.webPublicationDate.exists(afterFeedChangeDate)) afterChange ++ beforeChange.take(100) } def apply(resps: Seq[ItemResponse], adFree: Boolean = false, imageResizerSalt: Option[String]): Node Or Failed = { val tag = resps.headOption.flatMap(_.tag) tag match { case Some(t) => val content = resps.flatMap(_.results.getOrElse(Nil)).toList toXml(t, appleExcessDownloadsWorkaround(content), adFree, imageResizerSalt) case None => Bad(Failed("tag not found", NotFound)) } } def toXml(tag: Tag, contents: List[Content], adFree: Boolean, imageResizerSalt: Option[String]): Node Or Failed = { val description = Filtering.description(tag.description.getOrElse("")) tag.podcast match { case Some(podcast) => Good { <rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0"> <channel> { if (!adFree) { <itunes:new-feed-url>{ s"${tag.webUrl}/podcast.xml" }</itunes:new-feed-url> } } <title>{ tag.webTitle }</title> <link>{ tag.webUrl }</link> <description>{ description }</description> <language>en-gb</language> <copyright>{ podcast.copyright }</copyright> <lastBuildDate> { DateSupport.toRssTimeFormat(DateTime.now) } </lastBuildDate> <ttl>15</ttl> { podcast.podcastType match { case Some(value) => <itunes:type>{ value }</itunes:type> case None => } } <itunes:owner> <itunes:email>userhelp@theguardian.com</itunes:email> <itunes:name>{ author }</itunes:name> </itunes:owner> <itunes:image href={ podcast.image.getOrElse("") }/> <itunes:author>{ author }</itunes:author> { if (podcast.explicit) <itunes:explicit>yes</itunes:explicit> } <itunes:keywords/> <itunes:summary>{ description }</itunes:summary> <image> <title>{ tag.webTitle }</title> <url>{ podcast.image.getOrElse("https://static.guim.co.uk/sitecrumbs/Guardian.gif") }</url> <link>{ tag.webUrl }</link> </image> { if (adFree) { <itunes:block>yes</itunes:block> } } { for (category <- podcast.categories.getOrElse(Nil)) yield new CategoryRss(category).toXml } { for { podcastContent <- contents asset <- getFirstAudioAsset(podcastContent) } yield new iTunesRssItem(podcastContent, tag.id, asset, adFree, podcast.podcastType, imageResizerSalt).toXml } </channel> </rss> } case None => Bad { Failed("podcast not found", NotFound) } } } private def getFirstAudioAsset(podcast: Content): Option[Asset] = { // should contain at least one audio asset for { elements <- podcast.elements element <- elements.headOption asset <- element.assets.find(_.`type` == AssetType.Audio) } yield asset } } class CategoryRss(val category: PodcastCategory) { def toXml: Node = { <itunes:category text={ category.main }> { category.sub match { case Some(s) => <itunes:category text={ s }/> case None => } } </itunes:category> } }