Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apple Config as singular to podcast #898

Merged
merged 14 commits into from
Mar 13, 2024
21 changes: 9 additions & 12 deletions app/jobs/publish_feed_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,15 @@ def publish_feed(podcast, feed)
end

def publish_apple(feed)
feed.apple_configs.map do |config|
if feed.publish_to_apple?(config)
res = PublishAppleJob.perform_now(config)
PublishingPipelineState.publish_apple!(feed.podcast)
res
end
rescue => e
NewRelic::Agent.notice_error(e)
res = PublishingPipelineState.error_apple!(feed.podcast)
raise e if config.sync_blocks_rss?
res
end
return unless feed.publish_to_apple?
res = PublishAppleJob.perform_now(feed.apple_config)
PublishingPipelineState.publish_apple!(feed.podcast)
res
rescue => e
NewRelic::Agent.notice_error(e)
res = PublishingPipelineState.error_apple!(feed.podcast)
raise e if feed.apple_config.sync_blocks_rss?
res
end

def publish_rss(podcast, feed)
Expand Down
37 changes: 14 additions & 23 deletions app/models/apple/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,31 @@ class Config < ApplicationRecord
DEFAULT_TITLE = "Apple Delegated Delivery Subscriptions"
DEFAULT_AUDIO_FORMAT = {"f" => "flac", "b" => 16, "c" => 2, "s" => 44100}.freeze

belongs_to :podcast, class_name: "Podcast"
belongs_to :public_feed, class_name: "Feed"
belongs_to :private_feed, class_name: "Feed"

belongs_to :key, class_name: "Apple::Key", optional: true

has_one :podcast, through: :public_feed

delegate :title, to: :podcast, prefix: "podcast"
delegate :id, to: :podcast, prefix: "podcast"

delegate :provider_id, to: :key
delegate :key_id, to: :key
delegate :key_pem, to: :key
delegate :key_pem_b64, to: :key

validates_presence_of :podcast
validates_presence_of :public_feed
validates_presence_of :private_feed

validates_associated :public_feed
validates_associated :private_feed

validates :podcast, uniqueness: true, allow_nil: false

validates :public_feed, uniqueness: {scope: :private_feed,
message: "can only have one config per public and private feed"}
validates :public_feed, exclusion: {in: ->(apple_credential) { [apple_credential.private_feed] }}

validate :one_config_per_podcast, on: :create
validate :feed_podcasts_match

def self.has_apple_config?(podcast)
Expand Down Expand Up @@ -61,19 +60,20 @@ def self.find_or_build_private_feed(podcast)
)
end

# TODO: this a helper for onboarding via console, retrofit when the UX catches up
def self.build_apple_config(podcast, key)
if has_apple_config?(podcast)
if podcast.apple_config
kookster marked this conversation as resolved.
Show resolved Hide resolved
Rails.logger.error("Found existing apple config for #{podcast.title}!")
Rails.logger.error("Do you want to continue? (y/N)")
raise "Stopping build_apple_config" if $stdin.gets.chomp.downcase != "y"
end

ac = podcast.default_feed.apple_configs.first || Apple::Config.new
ac.public_feed = podcast.default_feed
ac.private_feed = find_or_build_private_feed(podcast)
ac.key = key

ac
Apple::Config.new(
podcast: podcast,
public_feed: podcast.default_feed,
private_feed: find_or_build_private_feed(podcast),
key: key
)
end

def self.mark_as_delivered!(apple_publisher)
Expand Down Expand Up @@ -103,27 +103,18 @@ def self.setup_delegated_delivery(podcast, key: nil, apple_config: nil, apple_sh
mark_as_delivered!(pub)
end

def one_config_per_podcast
return unless public_feed.present? && private_feed.present?
return unless podcast.present?

if podcast.feeds.map(&:apple_configs).flatten.compact.present?
errors.add(:podcast, "can only have one apple config")
end
end

def feed_podcasts_match
return unless public_feed.present? && private_feed.present?

if public_feed.podcast != private_feed.podcast
if (public_feed.podcast_id != podcast_id) || (private_feed.podcast_id != podcast_id)
kookster marked this conversation as resolved.
Show resolved Hide resolved
errors.add(:public_feed, "must belong to the same podcast as the private feed")
end
end

def publish_to_apple?
return false unless key&.valid?

public_feed.publish_to_apple?(self)
public_feed.publish_to_apple?
kookster marked this conversation as resolved.
Show resolved Hide resolved
end

def build_publisher
Expand Down
9 changes: 9 additions & 0 deletions app/models/apple/show.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def self.get_show(api, show_id)
api.unwrap_response(resp)
end

def self.for_podcast(podcast)
kookster marked this conversation as resolved.
Show resolved Hide resolved
apple_config = podcast.apple_config
api = Apple::Api.from_apple_config(apple_config)

new(api: api,
public_feed: apple_config.public_feed,
private_feed: apple_config.private_feed)
end

def inspect
"#<Apple:Show:#{object_id} show_id=#{try(:apple_id) || "nil"}>"
end
Expand Down
11 changes: 8 additions & 3 deletions app/models/feed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ class Feed < ApplicationRecord
alias_attribute :tokens, :feed_tokens
accepts_nested_attributes_for :feed_tokens, allow_destroy: true, reject_if: ->(ft) { ft[:token].blank? }

has_many :apple_configs, autosave: true, dependent: :destroy, foreign_key: :public_feed_id,
class_name: "::Apple::Config"
has_one :apple_config,
autosave: true,
class_name: "::Apple::Config",
dependent: :destroy,
foreign_key: :public_feed_id,
inverse_of: :public_feed,
validate: true

has_many :feed_images, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :feed
has_many :itunes_images, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :feed
Expand Down Expand Up @@ -167,7 +172,7 @@ def normalize_category(cat)
cat.to_s.downcase.gsub(/[^ a-z0-9_-]/, "").gsub(/\s+/, " ").strip
end

def publish_to_apple?(apple_config)
def publish_to_apple?
apple_config.present? &&
apple_config.public_feed == self &&
apple_config.publish_enabled?
Expand Down
1 change: 1 addition & 0 deletions app/models/podcast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Podcast < ApplicationRecord
serialize :restrictions, JSON

has_one :default_feed, -> { default }, class_name: "Feed", validate: true, autosave: true, inverse_of: :podcast
has_one :apple_config, class_name: "::Apple::Config", validate: true, autosave: true, inverse_of: :podcast

has_many :episodes, -> { order("published_at desc") }, dependent: :destroy
has_many :feeds, dependent: :destroy
Expand Down
2 changes: 2 additions & 0 deletions db/migrate/20231011213406_remove_apple_excluded_category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def change
pub.poll!(eps)

Apple::Config.mark_as_delivered!(pub)
rescue => e
kookster marked this conversation as resolved.
Show resolved Hide resolved
Rails.logger.error("Error on RemoveAppleExcludedCategory", {error: e.message, backtrace: e.backtrace[0, 2].join("\n")})
end
end
end
19 changes: 19 additions & 0 deletions db/migrate/20231019121850_add_podcast_id_to_apple_configs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

# add an id to associate an apple_config to a single podcast
class AddPodcastIdToAppleConfigs < ActiveRecord::Migration[7.0]
def up
add_column :apple_configs, :podcast_id, :integer

# for each apple_config, find the podcast from the public_feed, and set it
::Apple::Config.all.each do |config|
next unless (pid = config.public_feed&.podcast_id)
config.update!(podcast_id: pid)
Rails.logger.info("Config updated", {apple_config: config.id, podcast: config.podcast_id, pid: pid})
end
end

def down
remove_column :apple_configs, :podcast_id
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPodcastAppleConfigUniqueConstraint < ActiveRecord::Migration[7.0]
def change
add_index :apple_configs, [:podcast_id], unique: true
kookster marked this conversation as resolved.
Show resolved Hide resolved
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions test/factories/apple_config_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
publish_enabled { true }
sync_blocks_rss { true }
key { build(:apple_key) }

transient do
podcast { build(:podcast) }
end
podcast

# set up the private and public feeds
before(:create) do |apple_config, evaluator|
Expand Down
2 changes: 1 addition & 1 deletion test/jobs/publish_apple_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
let(:podcast) { episode.podcast }
let(:feed) { create(:feed, podcast: podcast, slug: "adfree") }
let(:private_feed) { create(:private_feed, podcast: podcast) }
let(:apple_config) { create(:apple_config, public_feed: feed, private_feed: private_feed) }
let(:apple_config) { create(:apple_config, podcast: podcast, public_feed: feed, private_feed: private_feed) }

describe "publishing to apple" do
it "does not publish to apple unless publish_enabled?" do
Expand Down
26 changes: 13 additions & 13 deletions test/jobs/publish_feed_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,40 @@

describe "publishing to apple" do
it "does not schedule publishing to apple if there is no apple config" do
assert_equal [], feed.apple_configs
assert_equal [], job.publish_apple(feed)
assert_equal nil, feed.apple_config
assert_equal nil, job.publish_apple(feed)
end

describe "when the apple config is present" do
let(:apple_config) { create(:apple_config, public_feed: feed, private_feed: private_feed) }
let(:apple_config) { create(:apple_config, podcast: podcast, public_feed: feed, private_feed: private_feed) }

it "does not schedule publishing to apple if the config is marked as not publishable" do
apple_config.update!(publish_enabled: false)
assert_equal [apple_config], feed.apple_configs.reload
assert_equal [nil], job.publish_apple(feed)
assert_equal apple_config, feed.apple_config.reload
assert_equal nil, job.publish_apple(feed)
end

it "does run the apple publishing if the config is present and marked as publishable" do
assert_equal [apple_config], feed.apple_configs.reload
assert_equal apple_config, feed.apple_config.reload
PublishAppleJob.stub(:perform_now, :publishing_apple!) do
assert_equal [:publishing_apple!], job.publish_apple(feed)
assert_equal :publishing_apple!, job.publish_apple(feed)
end
end

it "Performs the apple publishing job based regardless of sync_blocks_rss flag" do
assert_equal [apple_config], feed.apple_configs.reload
assert_equal apple_config, feed.apple_config.reload

# stub the two possible ways the job can be called
# perform_later is not used.
PublishAppleJob.stub(:perform_later, :perform_later) do
PublishAppleJob.stub(:perform_now, :perform_now) do
apple_config.update!(sync_blocks_rss: true)

assert_equal [:perform_now], job.publish_apple(feed)
assert_equal :perform_now, job.publish_apple(feed)

apple_config.update!(sync_blocks_rss: false)
feed.reload
assert_equal [:perform_now], job.publish_apple(feed)
assert_equal :perform_now, job.publish_apple(feed)
end
end
end
Expand All @@ -128,7 +128,7 @@
PublishingPipelineState.start!(feed.podcast)
end
it "raises an error if the apple publishing fails" do
assert_equal [apple_config], feed.apple_configs.reload
assert_equal apple_config, feed.apple_config.reload

PublishAppleJob.stub(:perform_now, ->(*, **) { raise "some apple error" }) do
# it raises
Expand All @@ -139,8 +139,8 @@
end

it "does not raise an error if the apple publishing is not blocking RSS" do
assert_equal [apple_config], feed.apple_configs.reload
feed.apple_configs.first.update!(sync_blocks_rss: false)
assert_equal apple_config, feed.apple_config.reload
feed.apple_config.update!(sync_blocks_rss: false)

mock = Minitest::Mock.new
mock.expect(:call, nil, [RuntimeError])
Expand Down
39 changes: 25 additions & 14 deletions test/models/apple/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,36 @@
end

it "is unique to a public and private feed" do
f1 = create(:feed)
f2 = create(:feed)
f3 = create(:feed)
f4 = create(:feed)
podcast1 = create(:podcast)
podcast2 = create(:podcast)
f1 = create(:feed, podcast: podcast1)
f2 = create(:feed, podcast: podcast1)
f3 = create(:feed, podcast: podcast2)
f4 = create(:feed, podcast: podcast2)

c1 = create(:apple_config, public_feed: f1, private_feed: f2)
c1 = create(:apple_config, podcast: podcast1, public_feed: f1, private_feed: f2)
assert c1.valid?

c2 = build(:apple_config, public_feed: f1, private_feed: f2)
c2 = build(:apple_config, podcast: podcast1, public_feed: f1, private_feed: f2)
refute c2.valid?

c3 = build(:apple_config, public_feed: f1, private_feed: f3)
assert c3.valid?
c3 = build(:apple_config, podcast: podcast1, public_feed: f1, private_feed: f3)
refute c3.valid?

c4 = build(:apple_config, public_feed: f4, private_feed: f2)
assert c4.valid?
c4 = build(:apple_config, podcast: podcast2, public_feed: f4, private_feed: f2)
refute c4.valid?

c5 = build(:apple_config, public_feed: f1, private_feed: f1)
c5 = build(:apple_config, podcast: podcast1, public_feed: f1, private_feed: f1)
refute c5.valid?

c6 = build(:apple_config, podcast: podcast2, public_feed: f3, private_feed: f4)
assert c6.valid?

c7 = build(:apple_config, podcast: podcast2, public_feed: f4, private_feed: f3)
assert c7.valid?

c7 = build(:apple_config, podcast: podcast2, public_feed: f4, private_feed: f4)
refute c7.valid?
end

it "is unique to a podcast" do
Expand All @@ -45,12 +56,12 @@
f3 = create(:feed, podcast: podcast)
f4 = create(:feed, podcast: podcast)

c1 = create(:apple_config, public_feed: f1, private_feed: f2)
c1 = create(:apple_config, podcast: podcast, public_feed: f1, private_feed: f2)
assert c1.valid?

c2 = build(:apple_config, public_feed: f3, private_feed: f4)
c2 = build(:apple_config, podcast: podcast, public_feed: f3, private_feed: f4)
refute c2.valid?
assert_equal ["can only have one apple config"], c2.errors[:podcast]
assert_equal ["has already been taken"], c2.errors[:podcast]
end
end
end
2 changes: 1 addition & 1 deletion test/models/apple/publisher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
let(:podcast) { create(:podcast) }
let(:public_feed) { create(:feed, podcast: podcast, private: false) }
let(:private_feed) { create(:private_feed, podcast: podcast) }
let(:apple_config) { create(:apple_config, public_feed: public_feed, private_feed: private_feed) }
let(:apple_config) { create(:apple_config, podcast: podcast, public_feed: public_feed, private_feed: private_feed) }
let(:episode) { create(:episode, podcast: podcast) }
let(:apple_episode_api_response) { build(:apple_episode_api_response, apple_episode_id: "123") }
let(:apple_publisher) { apple_config.build_publisher }
Expand Down
Loading