Moved creating a new podcast to a separate ActiveJob #2952 (#3373)

* Podcasts reachable status #2952

* Specs for podcasts statuses

* Moved podcast episode create to a separate ActiveJob

* Use RssItemData wrapper in GetEpisode for consistency

* Reorganize services/podcasts
This commit is contained in:
Anna Buianova 2019-07-04 20:10:54 +03:00 committed by Ben Halpern
parent da224d56aa
commit c3ed38f832
15 changed files with 182 additions and 33 deletions

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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 { "<guid isPermaLink=\"false\">#{Faker::Internet.url}/2.mp3}</guid>" }
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

View file

@ -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) { "<guid isPermaLink=\"false\">http://podcast.example/file.mp3</guid>" }
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

View file

@ -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

View file

@ -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

View file

@ -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) { "<guid isPermaLink=\"false\">http://podcast.example/file.mp3</guid>" }
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

View file

@ -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])

View file

@ -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: "<guid isPermaLink=\"false\">http://podcast.example/file.mp3</guid>",
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

View file

@ -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)