diff --git a/app/helpers/podcasts_helper.rb b/app/helpers/podcasts_helper.rb index 5110dffce..55de64abc 100644 --- a/app/helpers/podcasts_helper.rb +++ b/app/helpers/podcasts_helper.rb @@ -5,6 +5,18 @@ module PodcastsHelper RSS_LANGUAGE_CODES = %w[af sq eu be bg ca zh-cn zh-tw hr cs da nl nl-be nl-nl en en-au en-bz en-ca en-ie en-jm en-nz en-ph en-za en-tt en-gb en-us en-zw et fo fi fr fr-be fr-ca fr-fr fr-lu fr-mc fr-ch gl gd de de-at de-de de-li de-lu de-ch el haw hu is in ga it it-it it-ch ja ko mk no pl pt pt-br pt-pt ro ro-mo ro-ro ru ru-mo ru-ru sr sk sl es es-ar es-bo es-cl es-co es-cr es-do es-ec es-sv es-gt es-hn es-mx es-ni es-pa es-py es-pe es-pr es-es es-uy es-ve sv sv-fi sv-se tr uk] + def feed_description(feed, podcast) + [feed.description, podcast.description].detect { |d| d.present? } || "" + end + + def episode_description(episode) + if episode.podcast.has_apple_feed? + episode.description_safe + else + episode.description_with_default + end + end + def full_contact(type, item) name = item.try("#{type}_name") email = item.try("#{type}_email") diff --git a/app/jobs/publish_public_feed_job.rb b/app/jobs/publish_public_feed_job.rb new file mode 100644 index 000000000..0f74414a1 --- /dev/null +++ b/app/jobs/publish_public_feed_job.rb @@ -0,0 +1,17 @@ +require "builder" + +class PublishPublicFeedJob < ApplicationJob + queue_as :feeder_publishing + + include PodcastsHelper + + attr_writer :publish_feed_job + + def perform(podcast) + publish_feed_job.save_file(podcast, podcast.public_feed) + end + + def publish_feed_job + @publish_feed_job ||= PublishFeedJob.new + end +end diff --git a/app/models/apple/episode.rb b/app/models/apple/episode.rb index 0d5312a1a..9d8a2c801 100644 --- a/app/models/apple/episode.rb +++ b/app/models/apple/episode.rb @@ -295,7 +295,7 @@ def episode_create_parameters guid: guid, title: feeder_episode.title, originalReleaseDate: feeder_episode.published_at.utc.iso8601, - description: feeder_episode.description_with_default, + description: feeder_episode.description_safe, websiteUrl: feeder_episode.url, explicit: explicit, episodeNumber: feeder_episode.episode_number, diff --git a/app/models/episode.rb b/app/models/episode.rb index e421b8189..b443ede94 100644 --- a/app/models/episode.rb +++ b/app/models/episode.rb @@ -15,6 +15,7 @@ class Episode < ApplicationRecord include ReleaseEpisodes MAX_SEGMENT_COUNT = 10 + MAX_DESCRIPTION_BYTES = 4000 VALID_ITUNES_TYPES = %w[full trailer bonus] DROP_DATE = "COALESCE(episodes.published_at, episodes.released_at)" @@ -42,7 +43,7 @@ class Episode < ApplicationRecord validates :podcast_id, :guid, presence: true validates :title, presence: true - validates :description, bytesize: {maximum: 4000}, if: -> { strict_validations && description_changed? } + validates :description, bytesize: {maximum: MAX_DESCRIPTION_BYTES}, if: -> { strict_validations && description_changed? } validates :url, http_url: true validates :original_guid, presence: true, uniqueness: {scope: :podcast_id}, allow_nil: true alias_error_messages :item_guid, :original_guid @@ -310,8 +311,13 @@ def sanitize_text self.original_guid = original_guid.strip if !original_guid.blank? && original_guid_changed? end + # This value is safe to use in the RSS and integrations def description_with_default - description || subtitle || title || "" + [description, subtitle, title].detect { |d| d.present? } || "" + end + + def description_safe + description_with_default.truncate_bytes(MAX_DESCRIPTION_BYTES, omission: "") end def feeder_cdn_host diff --git a/app/models/feed.rb b/app/models/feed.rb index b8c630f8a..8ce085370 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -54,6 +54,7 @@ class Feed < ApplicationRecord validates :enclosure_prefix, http_url: true validates :display_episodes_count, numericality: {only_integer: true, greater_than: 0}, allow_nil: true validates :display_full_episodes_count, numericality: {only_integer: true, greater_than: 0}, allow_nil: true + validates :description, bytesize: {maximum: Episode::MAX_DESCRIPTION_BYTES} after_initialize :set_defaults before_validation :sanitize_text diff --git a/app/models/feeds/apple_subscription.rb b/app/models/feeds/apple_subscription.rb index 2f37dc89b..6473bbe02 100644 --- a/app/models/feeds/apple_subscription.rb +++ b/app/models/feeds/apple_subscription.rb @@ -6,6 +6,8 @@ class Feeds::AppleSubscription < Feed after_initialize :set_defaults + after_create :republish_public_feed + has_one :apple_config, class_name: "::Apple::Config", dependent: :destroy, autosave: true, validate: true, inverse_of: :feed accepts_nested_attributes_for :apple_config, allow_destroy: true, reject_if: :all_blank @@ -31,6 +33,10 @@ def self.model_name Feed.model_name end + def republish_public_feed + PublishPublicFeedJob.perform_later(podcast) + end + def unchanged_defaults return unless persisted? diff --git a/app/models/podcast.rb b/app/models/podcast.rb index 5de3efdf2..c761627a8 100644 --- a/app/models/podcast.rb +++ b/app/models/podcast.rb @@ -73,6 +73,10 @@ def apple_config end end + def has_apple_feed? + feeds.apple.exists? + end + def reload(options = nil) remove_instance_variable(:@apple_config) if defined?(@apple_config) super diff --git a/app/views/podcasts/show.rss.builder b/app/views/podcasts/show.rss.builder index d68d5eb9d..9e75c5160 100644 --- a/app/views/podcasts/show.rss.builder +++ b/app/views/podcasts/show.rss.builder @@ -16,11 +16,7 @@ xml.rss "xmlns:atom" => "http://www.w3.org/2005/Atom", xml.copyright @podcast.copyright unless @podcast.copyright.blank? xml.webMaster @podcast.web_master unless @podcast.web_master.blank? - if @feed.description.present? - xml.description { xml.cdata!(@feed.description) } - elsif @podcast.description.present? - xml.description { xml.cdata!(@podcast.description) } - end + xml.description { xml.cdata!(feed_description(@feed, @podcast)) } xml.managingEditor @podcast.managing_editor unless @podcast.managing_editor.blank? @@ -112,7 +108,7 @@ xml.rss "xmlns:atom" => "http://www.w3.org/2005/Atom", xml.title(ep.title) xml.pubDate ep.published_at.utc.rfc2822 xml.link ep.url || ep.enclosure_url(@feed) - xml.description { xml.cdata!(ep.description_with_default) } + xml.description { xml.cdata!(episode_description(ep)) } # TODO: may not reflect the content_type/file_size of replaced media xml.enclosure(url: ep.enclosure_url(@feed), type: ep.media_content_type(@feed), length: ep.media_file_size) if ep.media? diff --git a/test/jobs/publish_public_feed_job_test.rb b/test/jobs/publish_public_feed_job_test.rb new file mode 100644 index 000000000..132b20d65 --- /dev/null +++ b/test/jobs/publish_public_feed_job_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +describe PublishPublicFeedJob do + let(:podcast) { create(:podcast) } + + let(:job) { PublishPublicFeedJob.new } + + describe "saving the public rss file" do + let(:stub_client) { Aws::S3::Client.new(stub_responses: true) } + + it "can call save_file on PublishFeedJob" do + job.publish_feed_job.stub(:s3_client, stub_client) do + refute_nil job.perform(podcast) + end + end + end +end diff --git a/test/models/episode_test.rb b/test/models/episode_test.rb index 8ed88097c..d484f82d8 100644 --- a/test/models/episode_test.rb +++ b/test/models/episode_test.rb @@ -39,6 +39,25 @@ assert e.valid? end + it "has a safe description for integrations" do + e = build_stubbed(:episode, segment_count: 2, published_at: nil, strict_validations: true) + e.description = "a" * 4001 + assert e.description.bytesize == 4001 + assert e.description_safe.bytesize == 4000 + end + + it "has a description with fallbacks" do + e = build_stubbed(:episode, segment_count: 2, published_at: nil, strict_validations: true) + e.title = "title" + e.subtitle = nil + e.description = "" + assert e.description_with_default == "title" + e.subtitle = "sub" + assert e.description_with_default == "sub" + e.description = "desc" + assert e.description_with_default == "desc" + end + it "validates unique original guids" do e1 = create(:episode, original_guid: "original") e2 = build(:episode, original_guid: "original", podcast: e1.podcast)