Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e2546c3
Reorder the publishing to encompass the delivery
svevang Nov 17, 2025
db88ea0
Guard for episode integraion feed inclusion
svevang Nov 18, 2025
5c6a377
Use the integration status for apple episodes
svevang Nov 18, 2025
c455d79
Retain the error state
svevang Nov 18, 2025
f8ee333
Smaller query
svevang Nov 18, 2025
31e5dbb
Use consistent interface
svevang Nov 18, 2025
1eae4fe
Probe for episode feed inclusion
svevang Nov 18, 2025
84f2698
Refactor tests
svevang Nov 18, 2025
612e798
Add :new translation
svevang Nov 18, 2025
7a487d0
Don't shadow the variables
svevang Nov 18, 2025
5856f25
Just use a single set of statuses
svevang Nov 18, 2025
c6541d2
Rails and log for all errors
svevang Nov 18, 2025
4ef48ff
Relax to allow display
svevang Nov 19, 2025
15d217e
Lint
svevang Nov 19, 2025
09b388d
Remove unreachable branch
svevang Nov 19, 2025
59e063b
Probe for error after status uploaded
svevang Nov 19, 2025
ab3db05
Include the last_updated
svevang Nov 19, 2025
572e169
Clarify the updated at
svevang Nov 19, 2025
963fe93
Consistent formating
svevang Nov 19, 2025
5c33b4b
Some test coverage for the episode_integration_updated_at
svevang Nov 20, 2025
e206684
Sets up an error badge with file processing errors
svevang Nov 20, 2025
c07dd95
Audio asset state is indeterminate
svevang Nov 20, 2025
5eb6a3c
Lint
svevang Nov 20, 2025
56dab7c
Remove dupe branches
svevang Nov 21, 2025
a5d05b0
Set up callbacks for integration episode errors state
svevang Nov 21, 2025
410e70c
Rename AppleDelivery -> AppleIntegration
svevang Nov 21, 2025
0ef3193
Dedupe template blocks
svevang Nov 21, 2025
ec593eb
Lint
svevang Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/assets/stylesheets/app/badge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
color: $black;
}

.prx-badge-not_publishable {
background: $gray-400;
color: $black;
}

.prx-badge-incomplete-published,
.prx-badge-invalid,
.prx-badge-not_found,
Expand Down
34 changes: 6 additions & 28 deletions app/helpers/episodes_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ def episode_explicit_options
end

def episode_integration_status(integration, episode)
return "not_publishable" unless episode.integration_feed_episode?(integration)

status = episode.episode_delivery_status(integration, true)

if !status
"not_found"
elsif status.new_record?
"new"
elsif !status.uploaded?
"incomplete"
elsif episode.integration_error_state?(integration)
"error"
elsif !status.delivered?
"processing"
elsif status.delivered?
"complete"
else
"not_found"
"complete"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing to a default of complete? I guess I worry that could show complete even when it is not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking both possibilities for delivered? boolean means that the else branch was unreachable code:

elsif !status.delivered?
"processing"
elsif status.delivered?
"complete"

Hence the rearrangement of the apple episode publishing invocation in this PR https://github.com/PRX/feeder.prx.org/pull/1384/files#diff-a2b7e8c0371fceca58ba0b83a4db37ef232d2770af6aeb616aaac0e3b0259876R148. Sort of a subtle difference, but I think makes sense to see delivered episodes as being "complete" here (already published).

end
end

Expand All @@ -38,31 +41,6 @@ def episode_integration_updated_at(integration, episode)
episode.updated_at
end

def episode_apple_status(episode)
apple_episode = episode.apple_episode
if !apple_episode
"not_found"
elsif apple_episode.apple_new?
"new"
elsif apple_episode.needs_delivery?
"incomplete"
elsif apple_episode.waiting_for_asset_state?
"processing"
elsif apple_episode.audio_asset_state_error?
"error"
elsif apple_episode.synced_with_apple?
"complete"
else
"not_found"
end
end

def episode_apple_updated_at(episode)
episode.apple_sync_log&.updated_at ||
episode.apple_status&.created_at ||
episode.updated_at
end

def episode_status_class(episode)
case episode.publishing_status_was
when "draft"
Expand Down
8 changes: 8 additions & 0 deletions app/models/apple/episode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,14 @@ def audio_asset_state_error?
audio_asset_state == AUDIO_ASSET_FAILURE
end

def delivery_file_errors?
podcast_delivery_files.any?(&:processed_errors?)
end

def error_state?
audio_asset_state_error? || delivery_file_errors?
end

def audio_asset_state_success?
audio_asset_state == AUDIO_ASSET_SUCCESS
end
Expand Down
17 changes: 8 additions & 9 deletions app/models/apple/publisher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,15 @@ def publish!

def upload_and_process!(eps)
Rails.logger.tagged("Apple::Publisher#upload_and_process!") do
eps.filter(&:apple_needs_upload?).each_slice(PUBLISH_CHUNK_LEN) do |eps|
upload_media!(eps)
eps.filter(&:apple_needs_upload?).each_slice(PUBLISH_CHUNK_LEN) do |batch|
upload_media!(batch)
end

eps.filter(&:apple_needs_delivery?).each_slice(PUBLISH_CHUNK_LEN) do |eps|
process_delivery!(eps)
eps.filter(&:apple_needs_delivery?).each_slice(PUBLISH_CHUNK_LEN) do |batch|
process_delivery!(batch)
end

eps.each_slice(PUBLISH_CHUNK_LEN) do |eps|
publish_drafting!(eps)
raise_delivery_processing_errors(eps)
end
raise_delivery_processing_errors(eps)
end
end

Expand Down Expand Up @@ -145,9 +142,11 @@ def process_delivery!(eps)
wait_for_upload_processing(eps)

# Wait for the audio asset to be processed by Apple
# Mark episodes as delivered as they are processed
wait_for_asset_state(eps) do |ready_eps|
log_asset_wait_duration!(ready_eps)
# Publish the ready episodes
publish_drafting!(ready_eps)
# Then mark them as delivered
mark_as_delivered!(ready_eps)
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "active_support/concern"

module AppleDelivery
module AppleIntegration
extend ActiveSupport::Concern

included do
Expand Down
2 changes: 1 addition & 1 deletion app/models/episode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Episode < ApplicationRecord
include PublishingStatus
include TextSanitizer
include EmbedPlayerHelper
include AppleDelivery
include AppleIntegration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is a better name I think

include ReleaseEpisodes

MAX_SEGMENT_COUNT = 10
Expand Down
4 changes: 4 additions & 0 deletions app/models/integrations/base/episode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def archived?
raise NotImplementedError, "Subclasses must implement archived?"
end

def error_state?
false
end

def ad_free?
feeder_episode.categories.include?("adfree")
end
Expand Down
25 changes: 25 additions & 0 deletions app/models/integrations/episode_integrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,36 @@ module Integrations::EpisodeIntegrations
end
end

def integration_episode(integration)
integration_episode_method = "#{integration}_episode"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good idea

if respond_to?(integration_episode_method)
send(integration_episode_method)
end
end

def publish_to_integration?(integration)
# see if there is an integration
podcast.feeds.any? { |f| f.integration_type == integration && f.publish_integration? }
end

def integration_feed_episode?(integration)
feed = integration_feed(integration)
publish_to_integration?(integration) && feed&.feed_episode_ids&.include?(id)
end

def integration_feed(integration)
podcast.feeds.find { |f| f.integration_type == integration }
end

def integration_error_state?(integration)
integration_episode_method = "#{integration}_episode"
if respond_to?(integration_episode_method)
send(integration_episode_method)&.error_state? || false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very clean, thank you!

else
false
end
end

def sync_log(integration)
sync_logs.send(integration.intern).order(updated_at: :desc).first
end
Expand Down
43 changes: 16 additions & 27 deletions app/views/episodes/_form_status.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,22 @@
<%= render "form_feeds", form: form, episode: episode %>
</div>

<% if episode.persisted? && episode.publish_to_apple? %>
<div class="col-12 mt-4">
<p class="status-text">
<strong><%= t(".apple_status") %>:</strong>
<span class="badge rounded-pill prx-badge-<%= episode_apple_status(episode) %>">
<%= t("helpers.label.episode.media_statuses.#{episode_apple_status(episode)}") %>
</span>
<br>
<%= local_time_ago(episode_apple_updated_at(episode)) %>
</p>
</div>
<% end %>
<% if episode.persisted? && episode.publish_to_integration?(:megaphone) %>
<% integration = :megaphone %>
<% integration_status = episode_integration_status(integration, episode) %>
<div class="col-12 mt-4">
<p class="status-text">
<strong><%= "#{integration.to_s.titleize} Status" %>:</strong>
<span class="badge rounded-pill prx-badge-<%= integration_status %>">
<%= t("helpers.label.episode.media_statuses.#{integration_status}") %>
</span>
<br>
<strong><%= t(".last_updated") %></strong>
<br>
<%= local_time_ago(episode_integration_updated_at(integration, episode)) %>
</p>
</div>
<% [:apple, :megaphone].each do |integration| %>
<% if episode.persisted? && episode.publish_to_integration?(integration) %>
<% integration_status = episode_integration_status(integration, episode) %>
<div class="col-12 mt-4">
<p class="status-text">
<strong><%= integration.to_s.titleize %> Status:</strong>
<span class="badge rounded-pill prx-badge-<%= integration_status %>">
<%= t("helpers.label.episode.media_statuses.#{integration_status}") %>
</span>
<br>
<strong><%= integration.to_s.titleize %> <%= t(".last_updated") %></strong>
<br>
<%= local_time_ago(episode_integration_updated_at(integration, episode)) %>
</p>
</div>
<% end %>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great

<% end %>
</div>

Expand Down
4 changes: 2 additions & 2 deletions app/views/episodes/_overview.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><%= t(".apple") %></span>
<span class="badge rounded-pill prx-badge-<%= episode_apple_status(episode) %>">
<%= t("helpers.label.episode.apple_statuses.#{episode_apple_status(episode)}") %>
<span class="badge rounded-pill prx-badge-<%= episode_integration_status(:apple, episode) %>">
<%= t("helpers.label.episode.media_statuses.#{episode_integration_status(:apple, episode)}") %>
</span>
</li>
<li class="list-group-item"><%= t(".spotify") %></li>
Expand Down
10 changes: 4 additions & 6 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,9 @@ en:
incomplete: Incomplete
incomplete_published: Incomplete
invalid: Invalid
new: New
not_found: Unknown
not_publishable: Not Included for Publishing
processing: Processing
medium: File Format
mediums:
Expand All @@ -401,12 +404,6 @@ en:
draft: Draft
published: Published
scheduled: Scheduled
apple_statuses:
complete: Complete
error: Error
incomplete: Incomplete
not_found: Unknown
processing: Processing
season_number: Season Number
segment_count: Number of Segments
subtitle: Subtitle
Expand Down Expand Up @@ -698,6 +695,7 @@ en:
title_scheduled: Scheduled
unpublish: Unpublish
apple_status: Apple Status
last_updated: Last Updated
form_tags:
title: Categories
help:
Expand Down
46 changes: 46 additions & 0 deletions test/controllers/episodes_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,50 @@ class EpisodesControllerTest < ActionDispatch::IntegrationTest
delete episode_url(episode)
assert_redirected_to podcast_episodes_url(podcast)
end

# Error Handling Tests

test "shows apple integration error state when asset processing fails" do
_apple_feed = create(:apple_feed, podcast: podcast)
error_episode = create(:episode, podcast: podcast, published_at: 1.hour.ago, segment_count: 1)

# Create delivery status showing uploaded but not delivered (processing state)
create(:apple_episode_delivery_status, episode: error_episode, uploaded: true, delivered: false)

# Create apple episode with error state
api_response = build(:apple_episode_api_response,
item_guid: error_episode.item_guid,
apple_hosted_audio_state: Apple::Episode::AUDIO_ASSET_FAILURE)
create(:apple_episode, feeder_episode: error_episode, api_response: api_response)

get edit_episode_url(error_episode)
assert_response :success
assert_select ".prx-badge-error", text: /Error/
end

test "shows error badge when delivery file has validation errors" do
_apple_feed = create(:apple_feed, podcast: podcast)
error_episode = create(:episode, podcast: podcast, published_at: 1.hour.ago, segment_count: 1)

# Create delivery status
create(:apple_episode_delivery_status, episode: error_episode, uploaded: true, delivered: false)

# Create apple episode without audio asset state (still indeterminate since file failed validation)
api_response = build(:apple_episode_api_response,
item_guid: error_episode.item_guid)
_apple_episode = create(:apple_episode, feeder_episode: error_episode, api_response: api_response)

# Create podcast container, delivery, and delivery file with validation error
container = create(:apple_podcast_container, episode: error_episode)
delivery = create(:apple_podcast_delivery, episode: error_episode, podcast_container: container)
pdf = create(:apple_podcast_delivery_file, episode: error_episode, podcast_delivery: delivery)

# Update the sync log with validation error
pdf.apple_sync_log.update!(**build(:podcast_delivery_file_api_response, asset_processing_state: "VALIDATION_FAILED"))
error_episode.apple_episode.podcast_delivery_files.reset

get edit_episode_url(error_episode)
assert_response :success
assert_select ".prx-badge-error", text: /Error/
end
end
Loading