Skip to content

Commit 0f6f102

Browse files
committed
Merge branch 'feat/podcast_scorecards_dashboard' into feat/conditional_category_cards
2 parents 2510248 + a96b228 commit 0f6f102

28 files changed

+443
-126
lines changed

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ PLATFORMS
577577
arm64-darwin-22
578578
arm64-darwin-23
579579
arm64-darwin-24
580+
arm64-darwin-25
580581
x86_64-darwin-21
581582
x86_64-darwin-22
582583
x86_64-darwin-23

app/assets/stylesheets/app/badge.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
color: $black;
2727
}
2828

29+
.prx-badge-not_publishable {
30+
background: $gray-400;
31+
color: $black;
32+
}
33+
2934
.prx-badge-incomplete-published,
3035
.prx-badge-invalid,
3136
.prx-badge-not_found,

app/controllers/api/auth/uploads_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def s3_signer
1212
end
1313

1414
def show
15-
opts = {bucket: s3_bucket, key: s3_key, use_accelerate_endpoint: s3_accelerate, expires_in: 30.minutes.to_i}
15+
opts = {bucket: s3_bucket, key: s3_key, use_accelerate_endpoint: s3_accelerate, expires_in: 1.hour.to_i}
1616
exp = Time.now.to_i + opts[:expires_in]
1717
put_url = s3_signer.presigned_request(:put_object, opts)
1818
get_url = s3_signer.presigned_request(:get_object, opts)

app/controllers/errors_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ class ErrorsController < ActionController::Base
88
def not_found
99
render file: Rails.public_path.join("404.html"), status: :not_found
1010
end
11+
12+
def not_allowed
13+
head :method_not_allowed
14+
end
1115
end

app/controllers/podcast_metrics_controller.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ def episode_sparkline
1313
@prev_episode = Episode.find_by(guid: metrics_params[:prev_episode_id])
1414

1515
@episode_trend = calculate_episode_trend(@episode, @prev_episode)
16+
1617
@sparkline_downloads =
1718
Rollups::HourlyDownload
18-
.where(episode_id: @episode[:guid], hour: (@episode.first_rss_published_at..Date.utc_today))
19+
.where(episode_id: @episode[:guid], hour: (publish_hour(@episode)..publish_hour(@episode) + 6.months))
1920
.final
2021
.select(:episode_id, "DATE_TRUNC('DAY', hour) AS hour", "SUM(count) AS count")
2122
.group(:episode_id, "DATE_TRUNC('DAY', hour) AS hour")
@@ -261,25 +262,33 @@ def metrics_params
261262
end
262263

263264
def calculate_episode_trend(episode, prev_episode)
264-
return nil unless episode.in_default_feed? && episode.first_rss_published_at.present? && prev_episode.present?
265+
return nil unless episode.first_rss_published_at.present? && prev_episode.present?
265266
return nil if (episode.first_rss_published_at + 1.day) > Time.now
266267

267268
ep_dropday_sum = episode_dropday_query(episode)
268269
previous_ep_dropday_sum = episode_dropday_query(prev_episode)
269270

270271
return nil if ep_dropday_sum <= 0 || previous_ep_dropday_sum <= 0
271272

272-
((ep_dropday_sum.to_f / previous_ep_dropday_sum.to_f) - 1).round(2)
273+
((ep_dropday_sum.to_f / previous_ep_dropday_sum.to_f) - 1).round(3)
273274
end
274275

275276
def episode_dropday_query(ep)
276-
lowerbound = ep.first_rss_published_at.beginning_of_hour
277+
lowerbound = publish_hour(ep)
277278
upperbound = lowerbound + 24.hours
278279

279280
Rollups::HourlyDownload
280-
.where(episode_id: ep[:guid], hour: (lowerbound..upperbound))
281+
.where(episode_id: ep[:guid], hour: (lowerbound...upperbound))
281282
.final
282283
.load_async
283284
.sum(:count)
284285
end
286+
287+
def publish_hour(episode)
288+
if episode.first_rss_published_at.present?
289+
episode.first_rss_published_at.beginning_of_hour
290+
else
291+
episode.published_at.beginning_of_hour
292+
end
293+
end
285294
end

app/controllers/podcasts_controller.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ def show
2020

2121
@recently_published_episodes = @podcast.episodes.published.dropdate_desc.limit(4)
2222
@trend_episodes = @podcast.default_feed.episodes.published.dropdate_desc.where.not(first_rss_published_at: nil).offset(1).limit(4)
23-
@episode_trend_pairs = episode_trend_pairs(@recently_published_episodes, @trend_episodes)
24-
@alltime_downloads = alltime_downloads(@podcast).sum(&:count)
25-
@daterange_downloads = daterange_downloads(@podcast).sum(&:count)
23+
if Rails.env.development?
24+
@episode_trend_pairs = episode_trend_pairs(@recently_published_episodes, @trend_episodes)
25+
@alltime_downloads = alltime_downloads(@podcast).sum(&:count)
26+
@daterange_downloads = daterange_downloads(@podcast).sum(&:count)
27+
end
2628
@episode_count = @podcast.episodes.published.length
2729

2830
# @recently_published is used for the prod branch

app/helpers/episodes_helper.rb

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@ def episode_explicit_options
1616
end
1717

1818
def episode_integration_status(integration, episode)
19+
return "not_publishable" unless episode.integration_feed_episode?(integration)
20+
1921
status = episode.episode_delivery_status(integration, true)
22+
2023
if !status
2124
"not_found"
2225
elsif status.new_record?
2326
"new"
2427
elsif !status.uploaded?
2528
"incomplete"
29+
elsif episode.integration_error_state?(integration)
30+
"error"
2631
elsif !status.delivered?
2732
"processing"
28-
elsif status.delivered?
29-
"complete"
3033
else
31-
"not_found"
34+
"complete"
3235
end
3336
end
3437

@@ -38,31 +41,6 @@ def episode_integration_updated_at(integration, episode)
3841
episode.updated_at
3942
end
4043

41-
def episode_apple_status(episode)
42-
apple_episode = episode.apple_episode
43-
if !apple_episode
44-
"not_found"
45-
elsif apple_episode.apple_new?
46-
"new"
47-
elsif apple_episode.needs_delivery?
48-
"incomplete"
49-
elsif apple_episode.waiting_for_asset_state?
50-
"processing"
51-
elsif apple_episode.audio_asset_state_error?
52-
"error"
53-
elsif apple_episode.synced_with_apple?
54-
"complete"
55-
else
56-
"not_found"
57-
end
58-
end
59-
60-
def episode_apple_updated_at(episode)
61-
episode.apple_sync_log&.updated_at ||
62-
episode.apple_status&.created_at ||
63-
episode.updated_at
64-
end
65-
6644
def episode_status_class(episode)
6745
case episode.publishing_status_was
6846
when "draft"

app/helpers/metrics_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ def parse_trend(trend)
1717

1818
if trend > 0
1919
{
20-
percent: "+#{(trend * 100).round(2)}%",
20+
percent: "+#{trend * 100}%",
2121
color: modified_trend_color(trend, "text-success"),
2222
direction: modified_trend_direction(trend, "up")
2323
}
2424
elsif trend < 0
2525
{
26-
percent: "-#{(trend * -100).round(2)}%",
26+
percent: "-#{trend * -100}%",
2727
color: modified_trend_color(trend, "text-danger"),
2828
direction: modified_trend_direction(trend, "down")
2929
}

app/models/apple/episode.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,14 @@ def audio_asset_state_error?
478478
audio_asset_state == AUDIO_ASSET_FAILURE
479479
end
480480

481+
def delivery_file_errors?
482+
podcast_delivery_files.any?(&:processed_errors?)
483+
end
484+
485+
def error_state?
486+
audio_asset_state_error? || delivery_file_errors?
487+
end
488+
481489
def audio_asset_state_success?
482490
audio_asset_state == AUDIO_ASSET_SUCCESS
483491
end

app/models/apple/publisher.rb

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,15 @@ def publish!
9191

9292
def upload_and_process!(eps)
9393
Rails.logger.tagged("Apple::Publisher#upload_and_process!") do
94-
eps.filter(&:apple_needs_upload?).each_slice(PUBLISH_CHUNK_LEN) do |eps|
95-
upload_media!(eps)
94+
eps.filter(&:apple_needs_upload?).each_slice(PUBLISH_CHUNK_LEN) do |batch|
95+
upload_media!(batch)
9696
end
9797

98-
eps.filter(&:apple_needs_delivery?).each_slice(PUBLISH_CHUNK_LEN) do |eps|
99-
process_delivery!(eps)
98+
eps.filter(&:apple_needs_delivery?).each_slice(PUBLISH_CHUNK_LEN) do |batch|
99+
process_delivery!(batch)
100100
end
101101

102-
eps.each_slice(PUBLISH_CHUNK_LEN) do |eps|
103-
publish_drafting!(eps)
104-
raise_delivery_processing_errors(eps)
105-
end
102+
raise_delivery_processing_errors(eps)
106103
end
107104
end
108105

@@ -145,9 +142,11 @@ def process_delivery!(eps)
145142
wait_for_upload_processing(eps)
146143

147144
# Wait for the audio asset to be processed by Apple
148-
# Mark episodes as delivered as they are processed
149145
wait_for_asset_state(eps) do |ready_eps|
150146
log_asset_wait_duration!(ready_eps)
147+
# Publish the ready episodes
148+
publish_drafting!(ready_eps)
149+
# Then mark them as delivered
151150
mark_as_delivered!(ready_eps)
152151
end
153152
end
@@ -398,11 +397,47 @@ def mark_as_uploaded!(eps)
398397
end
399398
end
400399

400+
def verify_publishing_state!(eps)
401+
# Capture initial state before polling to detect drift
402+
initial_states = eps.to_h { |ep| [ep.feeder_id, ep.publishing_state] }
403+
404+
poll_episodes!(eps)
405+
406+
drifted_count = eps.count do |ep|
407+
initial_state = initial_states[ep.feeder_id]
408+
(ep.publishing_state != initial_state).tap do |drifted|
409+
if drifted
410+
Rails.logger.warn("Episode publishing state found to be out of sync", {
411+
episode_id: ep.feeder_id,
412+
local_expected_state: initial_state,
413+
remote_actual_state: ep.publishing_state
414+
})
415+
end
416+
end
417+
end
418+
419+
if drifted_count.positive?
420+
raise Apple::RetryPublishingError.new("Detected #{drifted_count} episodes with publishing state drift")
421+
end
422+
423+
true
424+
end
425+
401426
def publish_drafting!(eps)
402427
Rails.logger.tagged("##{__method__}") do
403-
eps = eps.select { |ep| ep.drafting? && ep.container_upload_complete? }
428+
# Guard: verify all episodes are in a consistent local and remote state
429+
verify_publishing_state!(eps)
430+
431+
drafting_eps, non_ready_eps = eps.partition { |ep| ep.drafting? && ep.container_upload_complete? }
432+
non_ready_eps.each do |ep|
433+
Rails.logger.info("Skipping publish for non-ready episode", {episode_id: ep.feeder_id,
434+
publishing_state: ep.publishing_state,
435+
container_upload_complete: ep.container_upload_complete?})
436+
end
437+
438+
Rails.logger.info("Publishing #{drafting_eps.length} drafting episodes.", {drafting_episode_ids: drafting_eps.map(&:feeder_id)}) if drafting_eps.any?
404439

405-
res = Apple::Episode.publish(api, show, eps)
440+
res = Apple::Episode.publish(api, show, drafting_eps)
406441

407442
Rails.logger.info("Published #{res.length} drafting episodes.")
408443
end

0 commit comments

Comments
 (0)