diff --git a/app/jobs/podcast_episodes/create_job.rb b/app/jobs/podcast_episodes/create_job.rb new file mode 100644 index 000000000..0b63808b3 --- /dev/null +++ b/app/jobs/podcast_episodes/create_job.rb @@ -0,0 +1,11 @@ +module PodcastEpisodes + class CreateJob < ApplicationJob + queue_as :podcast_episode_create + + # @param podcast_id [Integer] - podcast id + # @param item [Hash] according to the Podcasts::EpisodeRssItem::ATTRIBUTES + def perform(podcast_id, item) + Podcasts::CreateEpisode.call(podcast_id, item) + end + end +end diff --git a/app/models/podcast.rb b/app/models/podcast.rb index 7f2075fc7..f94ffc030 100644 --- a/app/models/podcast.rb +++ b/app/models/podcast.rb @@ -22,7 +22,7 @@ class Podcast < ApplicationRecord alias_attribute :name, :title def existing_episode(item) - episode = PodcastEpisode.where(media_url: item.enclosure.url). + episode = PodcastEpisode.where(media_url: item.enclosure_url). or(PodcastEpisode.where(title: item.title)). or(PodcastEpisode.where(guid: item.guid.to_s)).presence episode ||= PodcastEpisode.where(website_url: item.link).presence if unique_website_url? diff --git a/app/services/podcasts/create_episode.rb b/app/services/podcasts/create_episode.rb index d85c3af4a..4e123b5ab 100644 --- a/app/services/podcasts/create_episode.rb +++ b/app/services/podcasts/create_episode.rb @@ -2,7 +2,7 @@ module Podcasts class CreateEpisode def initialize(podcast_id, item) @podcast_id = podcast_id - @item = item + @item = item.is_a?(EpisodeRssItem) ? item : EpisodeRssItem.new(item) end def self.call(*args) @@ -24,7 +24,7 @@ module Podcasts rescue StandardError => e Rails.logger.error("not a valid date: #{e}") end - ep.body = item.content_encoded || item.itunes_summary || item.description + ep.body = item.body ep.save! ep end @@ -35,14 +35,14 @@ module Podcasts # checking url when it is https is useless, the url is set to the enclosure url anyway def get_media_url(episode) - episode.media_url = if HTTParty.head(item.enclosure.url.gsub(/http:/, "https:")).code == 200 - item.enclosure.url.gsub(/http:/, "https:") + episode.media_url = if HTTParty.head(item.enclosure_url.gsub(/http:/, "https:")).code == 200 + item.enclosure_url.gsub(/http:/, "https:") else - item.enclosure.url + item.enclosure_url end rescue StandardError # podcast episode must have a media_url - episode.media_url = item.enclosure.url + episode.media_url = item.enclosure_url episode.podcast.update(status_notice: I18n.t(:unplayable, scope: "podcasts.statuses")) if episode.podcast.status_notice.empty? end end diff --git a/app/services/podcasts/episode_rss_item.rb b/app/services/podcasts/episode_rss_item.rb new file mode 100644 index 000000000..b84ed29cc --- /dev/null +++ b/app/services/podcasts/episode_rss_item.rb @@ -0,0 +1,33 @@ +# a wrapper/adapter for RSS::Rss::Channel::Item to be able to pass it to ActiveJob +module Podcasts + class EpisodeRssItem + ATTRIBUTES = %i[title itunes_subtitle itunes_summary link guid pubDate body enclosure_url].freeze + + attr_reader(*ATTRIBUTES) + + def initialize(attributes) + ATTRIBUTES.each do |key| + instance_variable_set("@#{key}", attributes[key]) + end + end + + def self.from_item(item) + new( + title: item.title, + itunes_subtitle: item.itunes_subtitle, + itunes_summary: item.itunes_summary, + link: item.link, + guid: item.guid.to_s, + pubDate: item.pubDate.to_s, + enclosure_url: item.enclosure.url, + body: item.content_encoded || item.itunes_summary || item.description, + ) + end + + def to_h + ATTRIBUTES.each_with_object({}) do |key, hash| + hash[key] = instance_variable_get("@#{key}") + end + end + end +end diff --git a/app/services/podcasts/get_episode.rb b/app/services/podcasts/get_episode.rb index fd5b75e9a..831de51ed 100644 --- a/app/services/podcasts/get_episode.rb +++ b/app/services/podcasts/get_episode.rb @@ -1,20 +1,22 @@ module Podcasts class GetEpisode - def initialize(podcast) + def initialize(podcast, update = Podcasts::UpdateEpisode) @podcast = podcast + @update = update end def call(item) - episode = podcast.existing_episode(item) + item_data = item.is_a?(EpisodeRssItem) ? item : Podcasts::EpisodeRssItem.from_item(item) + episode = podcast.existing_episode(item_data) if episode - Podcasts::UpdateEpisode.call(episode, item) + update.call(episode, item_data) else - Podcasts::CreateEpisode.call(podcast.id, item) + PodcastEpisodes::CreateJob.perform_later(podcast.id, item_data.to_h) end end private - attr_reader :podcast + attr_reader :podcast, :update end end diff --git a/app/services/podcasts/update_episode.rb b/app/services/podcasts/update_episode.rb index c11ed3aa6..ba9096b6b 100644 --- a/app/services/podcasts/update_episode.rb +++ b/app/services/podcasts/update_episode.rb @@ -11,7 +11,7 @@ module Podcasts def call update_published_at unless episode.published_at? - update_media_url if !episode.media_url.include?("https") && item.enclosure.url.include?("https") + update_media_url if !episode.media_url.include?("https") && item.enclosure_url.include?("https") end private @@ -26,7 +26,7 @@ module Podcasts end def update_media_url - episode.update!(media_url: item.enclosure.url) + episode.update!(media_url: item.enclosure_url) rescue StandardError message = "something went wrong with #{episode.podcast_title}, #{episode.title} -- #{episode.media_url}" Rails.logger.error(message) diff --git a/spec/factories/podcast_episode_rss_items.rb b/spec/factories/podcast_episode_rss_items.rb new file mode 100644 index 000000000..dc6feacf2 --- /dev/null +++ b/spec/factories/podcast_episode_rss_items.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :podcast_episode_rss_item, class: "Podcasts::EpisodeRssItem" do + title { Faker::Book.title } + itunes_subtitle { Faker::Hipster.words(3) } + itunes_summary { Faker::Hipster.words(3) } + link { Faker::Internet.url } + guid { "#{Faker::Internet.url}/2.mp3}" } + pubDate { Faker::Date.between(2.years.ago, Time.zone.today) } + body { Faker::Hipster.paragraph(1) } + enclosure_url { "#{Faker::Internet.url}/2.mp3" } + + initialize_with { new(attributes) } + end +end diff --git a/spec/jobs/podcast_episodes/create_job_spec.rb b/spec/jobs/podcast_episodes/create_job_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/spec/models/podcast_spec.rb b/spec/models/podcast_spec.rb index 3321e0f1c..4755d8229 100644 --- a/spec/models/podcast_spec.rb +++ b/spec/models/podcast_spec.rb @@ -67,14 +67,18 @@ RSpec.describe Podcast, type: :model do describe "#existing_episode" do let(:podcast) { create(:podcast) } - let(:enclosure) { instance_double("RSS::Rss::Channel::Item::Enclosure", url: "https://audio.simplecast.com/2330f132.mp3") } let(:guid) { "http://podcast.example/file.mp3" } + let(:item) do - instance_double("RSS::Rss::Channel::Item", pubDate: "2019-06-19", - enclosure: enclosure, - title: "lightalloy's podcast", - guid: guid, - link: "https://litealloy.ru") + build(:podcast_episode_rss_item, pubDate: "2019-06-19", + enclosure_url: "https://audio.simplecast.com/2330f132.mp3", + description: "yet another podcast", + title: "lightalloy's podcast", + guid: guid, + itunes_subtitle: "hello", + content_encoded: nil, + itunes_summary: "world", + link: "https://litealloy.ru") end it "determines existing episode by media_url" do diff --git a/spec/requests/podcast_episodes_api_spec.rb b/spec/requests/podcast_episodes_api_spec.rb index cea15438a..9fe668e38 100644 --- a/spec/requests/podcast_episodes_api_spec.rb +++ b/spec/requests/podcast_episodes_api_spec.rb @@ -9,7 +9,9 @@ RSpec.describe "ArticlesApi", type: :request, vcr: vcr_option do let(:podcast) { create(:podcast, feed_url: "http://softwareengineeringdaily.com/feed/podcast/") } before do - Podcasts::Feed.new.get_episodes(podcast, 2) + perform_enqueued_jobs do + Podcasts::Feed.new.get_episodes(podcast, 2) + end end describe "GET /api/articles" do diff --git a/spec/services/podcasts/create_episode_spec.rb b/spec/services/podcasts/create_episode_spec.rb index 64134008a..7515ad36d 100644 --- a/spec/services/podcasts/create_episode_spec.rb +++ b/spec/services/podcasts/create_episode_spec.rb @@ -6,10 +6,11 @@ RSpec.describe Podcasts::CreateEpisode, type: :service do let!(:podcast) { create(:podcast) } context "when item has an https media_url" do - let!(:item) { RSS::Parser.parse("spec/support/fixtures/podcasts/developertea.rss", false).items.first } + let(:rss_item) { RSS::Parser.parse("spec/support/fixtures/podcasts/developertea.rss", false).items.first } + let!(:item) { Podcasts::EpisodeRssItem.from_item(rss_item) } before do - stub_request(:head, item.enclosure.url).to_return(status: 200) + stub_request(:head, item.enclosure_url).to_return(status: 200) end it "creates an episode" do @@ -35,8 +36,9 @@ RSpec.describe Podcasts::CreateEpisode, type: :service do end context "when item has an http media url" do - let!(:item) { RSS::Parser.parse("spec/support/fixtures/podcasts/awayfromthekeyboard.rss", false).items.first } - let(:https_url) { "https://awayfromthekeyboard.com/wp-content/uploads/2018/02/Episode_075_Lara_Hogan_Demystifies_Public_Speaking.mp3" } + let(:rss_item) { RSS::Parser.parse("spec/support/fixtures/podcasts/awayfromthekeyboard.rss", false).items.first } + let!(:item) { Podcasts::EpisodeRssItem.from_item(rss_item) } + let(:https_url) { "https://awayfromthekeyboard.com/wp-content/uploads/2018/02/Episode_075_Lara_Hogan_Demystifies_Public_Speaking.mp3" } it "sets media_url to https version when it is available" do stub_request(:head, https_url).to_return(status: 200) @@ -47,7 +49,7 @@ RSpec.describe Podcasts::CreateEpisode, type: :service do it "keeps an http media url when https version is not available" do stub_request(:head, https_url).to_return(status: 404) episode = described_class.call(podcast.id, item) - expect(episode.media_url).to eq(item.enclosure.url) + expect(episode.media_url).to eq(item.enclosure_url) end # enable when the logic will not rely solely on exception diff --git a/spec/services/podcasts/episode_rss_item_spec.rb b/spec/services/podcasts/episode_rss_item_spec.rb new file mode 100644 index 000000000..a86bfc822 --- /dev/null +++ b/spec/services/podcasts/episode_rss_item_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" + +RSpec.describe Podcasts::EpisodeRssItem do + let(:enclosure) { instance_double("RSS::Rss::Channel::Item::Enclosure", url: "https://audio.simplecast.com/2330f132.mp3") } + let(:guid) { "http://podcast.example/file.mp3" } + let(:item) do + instance_double("RSS::Rss::Channel::Item", pubDate: "2019-06-19", + enclosure: enclosure, + description: "yet another podcast", + title: "lightalloy's podcast", + guid: guid, + itunes_subtitle: "hello", + content_encoded: nil, + itunes_summary: "world", + link: "https://litealloy.ru") + end + + describe "#new" do + it "create a nice object" do + data = described_class.new(title: "a", itunes_subtitle: "b", itunes_summary: "c", + link: "https://example.com", guid: "guid", pubDate: "2019-01-01", + body: "100", enclosure_url: "example.example") + expect(data.title).to eq("a") + end + end + + describe "#from_item" do + it "returns a hash" do + attributes = described_class.from_item(item).to_h + expect(attributes).to be_kind_of(Hash) + expect(attributes[:title]).to eq("lightalloy's podcast") + expect(attributes[:enclosure_url]).to eq("https://audio.simplecast.com/2330f132.mp3") + expect(attributes[:body]).to eq("world") + end + + it "has attr readers" do + data = described_class.from_item(item) + expect(data.guid).to eq(guid) + expect(data.enclosure_url).to eq("https://audio.simplecast.com/2330f132.mp3") + expect(data.link).to eq(item.link) + end + end +end diff --git a/spec/services/podcasts/feed_spec.rb b/spec/services/podcasts/feed_spec.rb index b5e85caea..a76b3a1de 100644 --- a/spec/services/podcasts/feed_spec.rb +++ b/spec/services/podcasts/feed_spec.rb @@ -55,12 +55,16 @@ RSpec.describe Podcasts::Feed, vcr: vcr_option do it "fetches podcast episodes" do expect do - described_class.new.get_episodes(podcast, 2) + perform_enqueued_jobs do + described_class.new.get_episodes(podcast, 2) + end end.to change(PodcastEpisode, :count).by(2) end it "fetches correct podcasts" do - described_class.new.get_episodes(podcast, 2) + perform_enqueued_jobs do + described_class.new.get_episodes(podcast, 2) + end episodes = podcast.podcast_episodes expect(episodes.pluck(:title).sort).to eq(["Analyse Asia with Bernard Leong", "IFTTT Architecture with Nicky Leach"]) expect(episodes.pluck(:media_url).sort).to eq(%w[https://traffic.libsyn.com/sedaily/AnalyseAsia.mp3 https://traffic.libsyn.com/sedaily/IFTTT.mp3]) diff --git a/spec/services/podcasts/get_episode_spec.rb b/spec/services/podcasts/get_episode_spec.rb new file mode 100644 index 000000000..261f678e5 --- /dev/null +++ b/spec/services/podcasts/get_episode_spec.rb @@ -0,0 +1,32 @@ +require "rails_helper" + +RSpec.describe Podcasts::GetEpisode do + let(:podcast) { create(:podcast) } + let(:episode) { create(:podcast_episode, podcast: podcast) } + let(:item) do + build(:podcast_episode_rss_item, pubDate: "2019-06-19", + enclosure_url: "https://audio.simplecast.com/2330f132.mp3", + description: "yet another podcast", + title: "lightalloy's podcast", + guid: "http://podcast.example/file.mp3", + itunes_subtitle: "hello", + content_encoded: nil, + itunes_summary: "world", + link: "https://litealloy.ru") + end + + it "calls UpdateEpisode when an episode exists" do + update = double + allow(update).to receive(:call) + allow(podcast).to receive(:existing_episode).and_return(episode) + described_class.new(podcast, update).call(item) + expect(update).to have_received(:call).with(episode, item) + end + + it "schedules a Create job when episode doesn't exist" do + allow(podcast).to receive(:existing_episode).and_return(nil) + expect do + described_class.new(podcast).call(item) + end.to have_enqueued_job.on_queue("podcast_episode_create") # .with(podcast.id) + end +end diff --git a/spec/services/podcasts/update_episode_spec.rb b/spec/services/podcasts/update_episode_spec.rb index cc1d1b99f..544a058ef 100644 --- a/spec/services/podcasts/update_episode_spec.rb +++ b/spec/services/podcasts/update_episode_spec.rb @@ -2,8 +2,10 @@ require "rails_helper" RSpec.describe Podcasts::UpdateEpisode, type: :service do let(:podcast) { create(:podcast) } - let(:enclosure) { instance_double("RSS::Rss::Channel::Item::Enclosure", url: "https://audio.simplecast.com/2330f132.mp3") } - let(:item) { instance_double("RSS::Rss::Channel::Item", pubDate: "2019-06-19", enclosure: enclosure) } + + let(:item) do + build(:podcast_episode_rss_item, enclosure_url: "https://example.com/1.mp3") + end it "updates published_at if it's nil" do episode = create(:podcast_episode, podcast: podcast, published_at: nil) @@ -13,14 +15,14 @@ RSpec.describe Podcasts::UpdateEpisode, type: :service do end it "updates media_url if the item url contains https" do - episode = create(:podcast_episode, podcast: podcast, media_url: "http://audio.simplecast.com/2330f132.mp3") + episode = create(:podcast_episode, podcast: podcast, media_url: "http://example.com/1.mp3") described_class.call(episode, item) episode.reload - expect(episode.media_url).to eq("https://audio.simplecast.com/2330f132.mp3") + expect(episode.media_url).to eq("https://example.com/1.mp3") end it "catches expception when pubDate is invalid" do - invalid_item = instance_double("RSS::Rss::Channel::Item", pubDate: "i'm not a date", enclosure: enclosure) + invalid_item = build(:podcast_episode_rss_item, pubDate: "I'm not a date") episode = create(:podcast_episode, podcast: podcast, published_at: nil) described_class.call(episode, invalid_item)