-
Notifications
You must be signed in to change notification settings - Fork 0
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
Override enclosure url and/or prefix #1163
base: main
Are you sure you want to change the base?
Changes from all commits
3be1c08
d8d454c
7e9482d
59e211d
4686812
ce9a4ed
ba82e6f
064c6ff
57a8701
8d08165
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,14 +4,99 @@ module EpisodeMedia | |
extend ActiveSupport::Concern | ||
|
||
included do | ||
enum :medium, [:audio, :uncut, :video, :override], prefix: true | ||
|
||
# NOTE: this just-in-time creates new media versions | ||
# TODO: convert to sql, so we don't have to load/check every episode? | ||
# TODO: stop loading non-latest media versions | ||
scope :feed_ready, -> { includes(media_versions: :media_resources).select { |e| e.feed_ready? } } | ||
|
||
has_many :media_versions, -> { order("created_at DESC") }, dependent: :destroy | ||
has_many :contents, -> { order("position ASC, created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :episode | ||
has_one :uncut, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :episode | ||
has_one :external_media_resource, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :episode | ||
|
||
accepts_nested_attributes_for :contents, allow_destroy: true, reject_if: ->(c) { c[:id].blank? && c[:original_url].blank? } | ||
accepts_nested_attributes_for :uncut, allow_destroy: true, reject_if: ->(u) { u[:id].blank? && u[:original_url].blank? } | ||
|
||
validate :validate_media_ready, if: :strict_validations | ||
|
||
after_save :destroy_out_of_range_contents, if: ->(e) { e.segment_count_previously_changed? } | ||
after_save :analyze_external_media | ||
end | ||
|
||
def validate_media_ready | ||
return unless published_at.present? && media? | ||
|
||
# media must be complete on _initial_ publish | ||
# otherwise - having files in any status is good enough | ||
is_ready = | ||
if published_at_was.blank? | ||
media_ready?(true) || override_ready? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why make a separate method, instead of just including the override logic in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, reading further... it feels like |
||
elsif medium_uncut? | ||
uncut.present? && !uncut.marked_for_destruction? | ||
elsif override? | ||
external_media_ready? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this makes sense... so for an already-published episode, you can't alter the external media? Because it looks like this requires it to be status=complete. (And I think this |
||
else | ||
media_ready?(false) | ||
end | ||
|
||
unless is_ready | ||
errors.add(:base, :media_not_ready, message: "media not ready") | ||
end | ||
end | ||
|
||
def medium=(new_medium) | ||
super | ||
|
||
if medium_changed? && medium_was.present? | ||
if medium_was == "uncut" && medium == "audio" | ||
uncut&.mark_for_destruction | ||
elsif medium_was == "audio" && medium == "uncut" | ||
if (c = contents.first) | ||
build_uncut.tap do |u| | ||
u.file_size = contents.first.file_size | ||
u.duration = contents.first.duration | ||
|
||
# use the feeder cdn url for older completed files | ||
is_old = (Time.now - c.created_at) > 24.hours | ||
u.original_url = (c.status_complete? && is_old) ? c.url : c.original_url | ||
end | ||
end | ||
contents.each(&:mark_for_destruction) | ||
else | ||
contents.each(&:mark_for_destruction) | ||
end | ||
end | ||
|
||
self.segment_count = 1 if medium_video? || medium_override? | ||
end | ||
|
||
def copy_media(force = false) | ||
contents.each { |c| c.copy_media(force) } | ||
images.each { |i| i.copy_media(force) } | ||
transcript&.copy_media(force) | ||
uncut&.copy_media(force) | ||
end | ||
|
||
def segment_range | ||
1..segment_count.to_i | ||
end | ||
|
||
def build_contents | ||
segment_range.map do |p| | ||
contents.find { |c| c.position == p } || contents.build(position: p) | ||
end | ||
end | ||
|
||
def destroy_out_of_range_contents | ||
if segment_count.present? && segment_count.positive? | ||
contents.where.not(position: segment_range.to_a).destroy_all | ||
end | ||
end | ||
|
||
def feed_ready? | ||
!media? || complete_media? | ||
!media? || complete_media? || override_ready? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like |
||
end | ||
|
||
def cut_media_version! | ||
|
@@ -97,7 +182,9 @@ def media_content_type(feed = nil) | |
feed_content_type = feed.try(:mime_type) | ||
|
||
# if audio AND feed has a mime type, dovetail will transcode to that | ||
if (media_content_type || "").starts_with?("audio") | ||
if override? | ||
external_media_resource&.mime_type || "audio/mpeg" | ||
elsif (media_content_type || "").starts_with?("audio") | ||
feed_content_type || media_content_type | ||
elsif media_content_type | ||
media_content_type | ||
|
@@ -111,11 +198,19 @@ def video_content_type?(*args) | |
end | ||
|
||
def media_duration | ||
media.inject(0.0) { |s, c| s + c.duration.to_f } + podcast.try(:duration_padding).to_f | ||
if override? | ||
external_media_resource&.duration | ||
else | ||
media.inject(0.0) { |s, c| s + c.duration.to_f } + podcast.try(:duration_padding).to_f | ||
end | ||
end | ||
|
||
def media_file_size | ||
media.inject(0) { |s, c| s + c.file_size.to_i } | ||
if override? | ||
external_media_resource&.file_size | ||
else | ||
media.inject(0) { |s, c| s + c.file_size.to_i } | ||
end | ||
end | ||
|
||
def media_ready?(must_be_complete = true) | ||
|
@@ -131,19 +226,48 @@ def media_ready?(must_be_complete = true) | |
end | ||
|
||
def media_status | ||
states = media.map(&:status).uniq | ||
states = if override? | ||
[external_media_resource&.status] | ||
else | ||
media.map(&:status).uniq | ||
end | ||
if !(%w[started created processing retrying] & states).empty? | ||
"processing" | ||
elsif states.any? { |s| s == "error" } | ||
"error" | ||
elsif states.any? { |s| s == "invalid" } | ||
"invalid" | ||
elsif media_ready? | ||
elsif media_ready? || override_ready? | ||
"complete" | ||
end | ||
end | ||
|
||
def media_url | ||
media.first.try(:href) | ||
end | ||
|
||
def override_ready? | ||
override? && external_media_ready? | ||
end | ||
|
||
def override_processing? | ||
override? && !external_media_ready? | ||
end | ||
|
||
def override? | ||
medium_override? || !enclosure_override_url.blank? | ||
end | ||
|
||
def external_media_ready? | ||
external_media_resource&.status_complete? | ||
end | ||
|
||
def analyze_external_media | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like this could be wrapped into the existing |
||
if enclosure_override_url.blank? | ||
external_media_resource&.destroy | ||
elsif enclosure_override_url != external_media_resource&.original_url | ||
external_media_resource&.destroy | ||
create_external_media_resource(original_url: enclosure_override_url) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,22 +23,15 @@ class Episode < ApplicationRecord | |
|
||
acts_as_paranoid | ||
|
||
serialize :overrides, coder: HashSerializer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did this just go away? |
||
|
||
belongs_to :podcast, -> { with_deleted }, touch: true | ||
|
||
has_many :episode_imports | ||
has_many :contents, -> { order("position ASC, created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :episode | ||
has_many :media_versions, -> { order("created_at DESC") }, dependent: :destroy | ||
has_many :images, -> { order("created_at DESC") }, class_name: "EpisodeImage", autosave: true, dependent: :destroy, inverse_of: :episode | ||
|
||
has_one :ready_image, -> { complete_or_replaced.order("created_at DESC") }, class_name: "EpisodeImage" | ||
has_one :uncut, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :episode | ||
has_one :transcript, -> { order("created_at DESC") }, dependent: :destroy, inverse_of: :episode | ||
|
||
accepts_nested_attributes_for :contents, allow_destroy: true, reject_if: ->(c) { c[:id].blank? && c[:original_url].blank? } | ||
accepts_nested_attributes_for :images, allow_destroy: true, reject_if: ->(i) { i[:id].blank? && i[:original_url].blank? } | ||
accepts_nested_attributes_for :uncut, allow_destroy: true, reject_if: ->(u) { u[:id].blank? && u[:original_url].blank? } | ||
accepts_nested_attributes_for :transcript, allow_destroy: true, reject_if: ->(t) { t[:id].blank? && t[:original_url].blank? } | ||
|
||
validates :podcast_id, :guid, presence: true | ||
|
@@ -53,12 +46,10 @@ class Episode < ApplicationRecord | |
validates :explicit, inclusion: {in: %w[true false]}, allow_nil: true | ||
validates :segment_count, presence: true, if: :strict_validations | ||
validates :segment_count, numericality: {only_integer: true, greater_than: 0, less_than_or_equal_to: MAX_SEGMENT_COUNT}, allow_nil: true | ||
validate :validate_media_ready, if: :strict_validations | ||
|
||
before_validation :set_defaults, :set_external_keyword, :sanitize_text | ||
|
||
after_save :publish_updated, if: ->(e) { e.published_at_previously_changed? } | ||
after_save :destroy_out_of_range_contents, if: ->(e) { e.segment_count_previously_changed? } | ||
|
||
scope :published, -> { where("episodes.published_at IS NOT NULL AND episodes.published_at <= now()") } | ||
scope :published_by, ->(offset) { where("episodes.published_at IS NOT NULL AND episodes.published_at <= ?", Time.now - offset) } | ||
|
@@ -71,8 +62,6 @@ class Episode < ApplicationRecord | |
scope :dropdate_asc, -> { reorder(Arel.sql("#{DROP_DATE} ASC NULLS FIRST")) } | ||
scope :dropdate_desc, -> { reorder(Arel.sql("#{DROP_DATE} DESC NULLS LAST")) } | ||
|
||
enum :medium, [:audio, :uncut, :video], prefix: true | ||
|
||
alias_attribute :number, :episode_number | ||
alias_attribute :season, :season_number | ||
|
||
|
@@ -198,36 +187,6 @@ def url_was | |
super || embed_player_landing_url(podcast, self) | ||
end | ||
|
||
def medium=(new_medium) | ||
super | ||
|
||
if medium_changed? && medium_was.present? | ||
if medium_was == "uncut" && medium == "audio" | ||
uncut&.mark_for_destruction | ||
elsif medium_was == "audio" && medium == "uncut" | ||
if (c = contents.first) | ||
build_uncut.tap do |u| | ||
u.file_size = contents.first.file_size | ||
u.duration = contents.first.duration | ||
|
||
# use the feeder cdn url for older completed files | ||
is_old = (Time.now - c.created_at) > 24.hours | ||
u.original_url = (c.status_complete? && is_old) ? c.url : c.original_url | ||
end | ||
end | ||
contents.each(&:mark_for_destruction) | ||
else | ||
contents.each(&:mark_for_destruction) | ||
end | ||
end | ||
|
||
self.segment_count = 1 if medium_video? | ||
end | ||
|
||
def overrides | ||
self[:overrides] ||= HashWithIndifferentAccess.new | ||
end | ||
|
||
def categories | ||
self[:categories] || [] | ||
end | ||
|
@@ -236,13 +195,6 @@ def categories=(cats) | |
self[:categories] = sanitize_categories(cats, false).presence | ||
end | ||
|
||
def copy_media(force = false) | ||
contents.each { |c| c.copy_media(force) } | ||
images.each { |i| i.copy_media(force) } | ||
transcript&.copy_media(force) | ||
uncut&.copy_media(force) | ||
end | ||
|
||
def publish! | ||
Rails.logger.tagged("Episode#publish!") do | ||
apple_mark_for_reupload! | ||
|
@@ -302,22 +254,6 @@ def feeder_cdn_host | |
ENV["FEEDER_CDN_HOST"] | ||
end | ||
|
||
def segment_range | ||
1..segment_count.to_i | ||
end | ||
|
||
def build_contents | ||
segment_range.map do |p| | ||
contents.find { |c| c.position == p } || contents.build(position: p) | ||
end | ||
end | ||
|
||
def destroy_out_of_range_contents | ||
if segment_count.present? && segment_count.positive? | ||
contents.where.not(position: segment_range.to_a).destroy_all | ||
end | ||
end | ||
|
||
def published_or_released_date | ||
if published_at.present? | ||
published_at | ||
|
@@ -326,25 +262,6 @@ def published_or_released_date | |
end | ||
end | ||
|
||
def validate_media_ready | ||
return unless published_at.present? && media? | ||
|
||
# media must be complete on _initial_ publish | ||
# otherwise - having files in any status is good enough | ||
is_ready = | ||
if published_at_was.blank? | ||
media_ready?(true) | ||
elsif medium_uncut? | ||
uncut.present? && !uncut.marked_for_destruction? | ||
else | ||
media_ready?(false) | ||
end | ||
|
||
unless is_ready | ||
errors.add(:base, :media_not_ready, message: "media not ready") | ||
end | ||
end | ||
|
||
def ready_transcript | ||
transcript if transcript&.status_complete? | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Been thinking about this... and I guess I'd prefer all the associations stay in the actual model? Having them scattered in concerns just makes it too hard to eyeball the structure of things.
I could be convinced though, so feel free to fight it out!