* 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:
parent
da224d56aa
commit
c3ed38f832
15 changed files with 182 additions and 33 deletions
11
app/jobs/podcast_episodes/create_job.rb
Normal file
11
app/jobs/podcast_episodes/create_job.rb
Normal 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
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
33
app/services/podcasts/episode_rss_item.rb
Normal file
33
app/services/podcasts/episode_rss_item.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
14
spec/factories/podcast_episode_rss_items.rb
Normal file
14
spec/factories/podcast_episode_rss_items.rb
Normal 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
|
||||
0
spec/jobs/podcast_episodes/create_job_spec.rb
Normal file
0
spec/jobs/podcast_episodes/create_job_spec.rb
Normal 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
43
spec/services/podcasts/episode_rss_item_spec.rb
Normal file
43
spec/services/podcasts/episode_rss_item_spec.rb
Normal 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
|
||||
|
|
@ -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])
|
||||
|
|
|
|||
32
spec/services/podcasts/get_episode_spec.rb
Normal file
32
spec/services/podcasts/get_episode_spec.rb
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue