Skip to content

Commit

Permalink
Merge pull request #622 from PRX/feat/async_validations
Browse files Browse the repository at this point in the history
Async validations
  • Loading branch information
cavis committed Apr 27, 2023
2 parents c23381b + f3be333 commit 98c3809
Show file tree
Hide file tree
Showing 44 changed files with 313 additions and 317 deletions.
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ gem "roar-rails", github: "PRX/roar-rails", branch: "feat/rails_7"
# models
gem "addressable"
gem "countries"
gem "fastimage"
gem "paranoia"
gem "sanitize"

Expand Down
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ GEM
railties (>= 5.0.0)
faraday (0.17.6)
multipart-post (>= 1.2, < 3)
fastimage (2.2.6)
ffi (1.15.5)
fuzzyurl (0.2.2)
globalid (1.0.1)
Expand Down Expand Up @@ -482,7 +481,6 @@ DEPENDENCIES
excon
factory_bot_rails
faraday (~> 0.17.4)
fastimage
hal_api-rails (~> 1.2.0)
hyperresource
importmap-rails
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,9 @@ def prx_auth_needs_refresh?(jwt_ttl)
def skip_session
request.session_options[:skip] = true
end

# TODO: hacky, but this method is private in turbo-rails
def turbo_frame_request?
request.headers["Turbo-Frame"].present?
end
end
5 changes: 0 additions & 5 deletions app/controllers/episodes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,4 @@ def episode_params
images_attributes: %i[id original_url size alt_text caption credit _destroy]
)
end

# TODO: hacky, but this method is private in turbo-rails
def turbo_frame_request?
request.headers["Turbo-Frame"].present?
end
end
11 changes: 11 additions & 0 deletions app/helpers/uploads_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,15 @@ def uploads_resolve_dev_credentials
raise e
end
end

def upload_invalid_messages(rec)
if rec.status_invalid?
rec.status = "complete"
rec.valid?
msgs = rec.errors.full_messages
rec.status = "invalid"
rec.valid?
msgs.present? && msgs.join(", ")
end
end
end
49 changes: 8 additions & 41 deletions app/models/concerns/image_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ module ImageFile

before_validation :initialize_attributes, on: :create

before_validation :detect_image_attributes

validates :original_url, presence: true

validates :format, inclusion: {in: ["jpeg", "png", "gif", nil]}
validates :format, inclusion: {in: %w[jpeg png gif]}, if: :status_complete?

enum status: [:started, :created, :processing, :complete, :error, :retrying, :cancelled]
enum :status, [:started, :created, :processing, :complete, :error, :retrying, :cancelled, :invalid], prefix: true

scope :complete_or_replaced, -> do
with_deleted
.complete
.status_complete
.where("deleted_at IS NULL OR replaced_at IS NOT NULL")
.order("created_at DESC")
end
Expand Down Expand Up @@ -63,7 +61,9 @@ def guid
end

def file_name
File.basename(URI.parse(original_url).path)
if original_url.present?
File.basename(URI.parse(original_url).path)
end
end

def copy_media(force = false)
Expand All @@ -75,11 +75,11 @@ def copy_media(force = false)
end

def url
complete? ? self[:url] : self[:original_url]
self[:url] ||= published_url
end

def href
complete? ? url : original_url
status_complete? ? url : original_url
end

def href=(h)
Expand All @@ -105,39 +105,6 @@ def reset_image_attributes
self.status = :created
end

def detect_image_attributes
# skip if we've already detected width/height/format
return if width.present? && height.present? && format.present?

# s3 urls cannot be fastimage'd - must wait for async porter inspection
return if original_url.blank? || original_url.starts_with?("s3://")

info = nil
begin
fastimage_options = {
timeout: 10,
raise_on_failure: true,
http_header: {"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X) PRX Feeder/1.0"}
}
info = FastImage.new(original_url, fastimage_options)
rescue FastImage::FastImageException => err
logger.error(err)
NewRelic::Agent.notice_error(err)
raise
end
self.dimensions = info.size
self.format = info.type
self.size = info.content_length
end

def dimensions
[width, height]
end

def dimensions=(s)
self.width, self.height = s
end

def replace?(img)
original_url != img.try(:original_url)
end
Expand Down
45 changes: 26 additions & 19 deletions app/models/concerns/porter_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,45 +63,52 @@ def porter_key(msg)

def porter_callback_media_meta
info = self.class.porter_callback_inspect(result).try(:[], :Inspection)
if info
mime = porter_callback_mime(info)
meta = {
mime_type: mime,
medium: (mime || "").split("/").first,
file_size: info[:Size].to_i
}
audio_meta = porter_callback_audio_meta(mime, info)
video_meta = porter_callback_video_meta(mime, info)

meta.merge(audio_meta).merge(video_meta)
end
mime = porter_callback_mime(info)
meta = {
mime_type: mime,
medium: (mime || "").split("/").first,
file_size: porter_callback_size(info)
}
audio_meta = porter_callback_audio_meta(mime, info)
video_meta = porter_callback_video_meta(mime, info)

meta.merge(audio_meta).merge(video_meta)
end

def porter_callback_image_meta
info = self.class.porter_callback_inspect(result).try(:[], :Inspection)
mime = porter_callback_mime(info || {})
mime = porter_callback_mime(info)
meta = {
size: porter_callback_size(info)
}

# only return for actual images - not detected images in id3 tags
if info && info[:Image] && mime.starts_with?("image/")
{
meta.merge(
format: info[:Image][:Format],
height: info[:Image][:Height].to_i,
size: info[:Size].to_i,
width: info[:Image][:Width].to_i
}
)
else
meta
end
end

def porter_callback_mime(info)
if info[:MIME]
if info && info[:MIME]
info[:MIME]
elsif info[:Audio]
elsif info && info[:Audio]
"audio/mpeg"
end
end

def porter_callback_size(info)
info[:Size].to_i if info
end

def porter_callback_audio_meta(mime, info)
if info[:Audio]
if info && info[:Audio]
{
sample_rate: info[:Audio][:Frequency].to_i,
channels: info[:Audio][:Channels].to_i,
Expand All @@ -115,7 +122,7 @@ def porter_callback_audio_meta(mime, info)

def porter_callback_video_meta(mime, info)
# only return for actual videos - not detected images in id3 tags
if info[:Video] && mime.starts_with?("video")
if info && info[:Video] && mime.starts_with?("video")
{
duration: info[:Video][:Duration].to_f / 1000,
bit_rate: info[:Video][:Bitrate].to_i / 1000,
Expand Down
1 change: 1 addition & 0 deletions app/models/enclosure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def update_with_enclosure!(enclosure)
def update_enclosure_attributes(enclosure)
self.file_size = enclosure["length"].to_i
self.mime_type = enclosure["type"]
self.medium = (enclosure["type"] || "").split("/").first
self.href = enclosure["url"]
self
end
Expand Down
5 changes: 4 additions & 1 deletion app/models/episode_image.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
class EpisodeImage < ActiveRecord::Base
include ImageFile

belongs_to :episode, touch: true, optional: true

include ImageFile
validates :height, :width, numericality: {greater_than_or_equal_to: 1400, less_than_or_equal_to: 3000}, if: :status_complete?
validates :width, comparison: {equal_to: :height}, if: :status_complete?

def destination_path
"#{episode.path}/#{image_path}"
Expand Down
8 changes: 2 additions & 6 deletions app/models/itunes_image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ class ITunesImage < ApplicationRecord
include ImageFile
include FeedImageFile

validates :height, :width, numericality: {
less_than_or_equal_to: 3000,
greater_than_or_equal_to: 1400
}, if: ->(i) { i.height && i.width }

validates :height, numericality: {equal_to: ->(image) { image.width }}, if: ->(i) { i.height }
validates :height, :width, numericality: {greater_than_or_equal_to: 1400, less_than_or_equal_to: 3000}, if: :status_complete?
validates :width, comparison: {equal_to: :height}, if: :status_complete?

def replace_resources!
ITunesImage.where(feed_id: feed_id).where.not(id: id).touch_all(:replaced_at, :deleted_at)
Expand Down
10 changes: 7 additions & 3 deletions app/models/media_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ class MediaResource < ApplicationRecord

acts_as_paranoid

enum status: [:started, :created, :processing, :complete, :error, :retrying, :cancelled]
enum :status, [:started, :created, :processing, :complete, :error, :retrying, :cancelled, :invalid], prefix: true

before_validation :initialize_attributes, on: :create

validates :original_url, presence: true

validates :medium, inclusion: {in: %w[audio video]}, if: :status_complete?

after_create :replace_resources!

scope :complete_or_replaced, -> do
with_deleted
.complete
.status_complete
.where("deleted_at IS NULL OR replaced_at IS NOT NULL")
.order("created_at DESC")
end
Expand Down Expand Up @@ -53,7 +57,7 @@ def replace_resources!
end

def href
complete? ? url : original_url
status_complete? ? url : original_url
end

def href=(h)
Expand Down
27 changes: 7 additions & 20 deletions app/models/tasks/copy_image_task.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
class Tasks::CopyImageTask < ::Task
before_save do
if image_resource && status_changed?
image_resource.status = status

if complete?
meta = porter_callback_image_meta
if meta
update_image!(meta.merge(url: image_resource.published_url))
podcast.try(:publish!)
else
update_image!
Rails.logger.warn("No image meta found in result: #{JSON.generate(result)}")
end
else
update_image!
image_resource.assign_attributes(porter_callback_image_meta)
image_resource.status = "invalid" if image_resource.invalid?
end

image_resource.save!
podcast.try(:publish!) if image_resource.status_complete?
end
end

Expand Down Expand Up @@ -46,14 +43,4 @@ def podcast
def image_resource
owner
end

private

def update_image!(attrs = {})
image_resource.update!(attrs.merge(status: status))
rescue ActiveRecord::RecordInvalid
# TODO: handle/display async validation issues
image_resource.restore_attributes
image_resource.update!(status: "error")
end
end
18 changes: 7 additions & 11 deletions app/models/tasks/copy_media_task.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
class Tasks::CopyMediaTask < ::Task
before_save do
if media_resource && status_changed?
media_resource.status = status

if complete?
meta = porter_callback_media_meta
if meta
media_resource.update!(meta.merge(status: status))
else
# TODO: should non-audio be an error or something?
media_resource.update!(status: status)
Rails.logger.warn("No audio meta found in result: #{JSON.generate(result)}")
end
episode.try(:podcast).try(:publish!)
else
media_resource.update!(status: status)
media_resource.assign_attributes(porter_callback_media_meta)
media_resource.status = "invalid" if media_resource.invalid?
end

media_resource.save!
episode.try(:podcast).try(:publish!) if media_resource.status_complete?
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/representers/api/episode_representer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Api::EpisodeRepresenter < Api::BaseRepresenter
decorator: Api::MediaResourceRepresenter,
class: MediaResource,
writeable: false,
if: ->(_o) { !media_resources&.all?(&:complete?) }
if: ->(_o) { !media_resources&.all?(&:status_complete?) }

property :image, decorator: Api::ImageRepresenter, class: EpisodeImage

Expand Down
1 change: 0 additions & 1 deletion app/representers/api/image_representer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ class Api::ImageRepresenter < Roar::Decorator
include HalApi::Representer::FormatKeys

property :href
property :url, writeable: false # TODO: deprecate in favor of href ... but Castle scrapes this field
property :original_url, writeable: false
property :alt_text
property :caption
Expand Down
2 changes: 1 addition & 1 deletion app/views/episodes/audio/_error.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<% end %>
</div>

<div class="invalid-feedback">There was a problem processing your audio</div>
<div class="invalid-feedback"><%= upload_invalid_messages(content) || "There was a problem processing your audio" %></div>

<label class="is-invalid">Segment <%= content.position %></label>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/views/images/_error.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<small class="text-muted">(<%= number_to_human_size(image.size) %>)</small>
</div>

<div class="invalid-feedback">There was a problem processing your image</div>
<div class="invalid-feedback"><%= upload_invalid_messages(image) || "There was a problem processing your image" %></div>

<label class="is-invalid">Image File</label>
</div>
Expand Down
Loading

0 comments on commit 98c3809

Please sign in to comment.