Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
23bd22f
feat(cra): presigned S3 URLs, progress tracking, multi-phase encoding
jirkamotejl Mar 17, 2026
091e3d6
fix(cra): review fixes — S3 dedup, race condition, state naming, tests
jirkamotejl Mar 17, 2026
e529c01
docs: add CRA encoding system design document
jirkamotejl Mar 17, 2026
8b99434
fix(cra): address code review issues — locks, safe defaults, query fix
jirkamotejl Mar 17, 2026
8d3bb32
fix(cra): move broadcasts outside lock in finalize_from_completed_pha…
jirkamotejl Mar 17, 2026
7bb5e7e
test: add coverage for failed-upload retry and orphan reconciliation
jirkamotejl Mar 17, 2026
c2b318c
docs: correct REMOVED status explanation, mark test gaps closed
jirkamotejl Mar 18, 2026
35f0cf1
fix: defer Redis I/O outside Postgres lock, remove dead code, update …
jirkamotejl Mar 18, 2026
c5b0a22
fix(test): remove S3::Client include from CreateFileJobTest
jirkamotejl Mar 18, 2026
ec06b27
fix(cra): schedule CheckProgressJob when remote_id matches but state …
jirkamotejl Mar 18, 2026
1e6fd5f
chore: remove superseded CRA plan files
jirkamotejl Mar 18, 2026
3a53315
docs: update cra-encoding-system.md — missing recovery paths, test co…
jirkamotejl Mar 18, 2026
2b70fba
test: close remaining CRA test coverage gaps
jirkamotejl Mar 18, 2026
38c4d34
feat: add video_poster_url provider-neutral hook to File::Video and C…
jirkamotejl Mar 19, 2026
4833088
refactor: remove CRA-specific knowledge from GenerateThumbnailJob
jirkamotejl Mar 19, 2026
13a7fcd
fix: set S3_BUCKET_NAME in create_media_job tests
jirkamotejl Mar 19, 2026
8ac04ac
fix: delegate generate_dragonfly_uid to Dragonfly's own datastore method
jirkamotejl Mar 19, 2026
0be829c
bump to 7.5.0
jirkamotejl Mar 19, 2026
becfacd
chore: update Gemfile.lock for 7.5.0
jirkamotejl Mar 19, 2026
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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [7.5.0] - 2026-03-19

### Added

- **CRA presigned S3 URLs**: Encoder no longer downloads video to local disk or uploads via SFTP. CRA fetches video directly from S3 via presigned URL (7-day expiry). Only the XML manifest is uploaded via SFTP.
- **Two-phase encoding**: When `encoder_processing_phases` > 1, CreateMediaJob submits two manifests with the same `refId` — SD first, then HD. Backward compatible: single-phase when `encoder_processing_phases` is nil/1.
- **Encoding progress tracking**: CheckProgressJob parses CRA `messages` array for per-phase milestones, extracts video duration, and estimates completion time. New processing states: `sd_processing → sd_processed → hd_processing → full_media_processed`.
- **Console encoding info component**: `EncodingInfoComponent` shows current encoding phase and progress percentage on video file detail page, with real-time updates via MessageBus.
- **S3 client and jobs**: `Folio::S3::Client` for presigned URL generation, `Folio::S3::CreateFileJob` for S3-based file creation, `Folio::File::GetVideoMetadataJob` for video metadata extraction.
- **Video thumbnail generation**: `GenerateThumbnailJob` reworked for reliable thumbnail generation from video files.

### Changed

- `ShowComponent` now exposes `aasmState` as a Stimulus value and reloads via Turbo on state transitions (encoding progress, file updates)
- `ShowComponent` layout: state badge moved to right side (`ms-auto`), encoding info rendered inline after state

### Fixed

- add `try` to `dont_run_after_save_jobs` to enable thumbnail generation for `private_attachments`
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ GIT
PATH
remote: .
specs:
folio (7.4.1)
folio (7.5.0)
aasm
activejob-uniqueness (>= 0.3.0)
acts-as-taggable-on
Expand Down
1 change: 1 addition & 0 deletions app/assets/javascripts/folio/console/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
//= require folio/console/files/picker/document_component
//= require folio/console/files/picker/image_component
//= require folio/console/files/picker_component
//= require folio/console/files/show/encoding_info_component
//= require folio/console/files/show/thumbnails/crop_edit_component
//= require folio/console/files/show_component
//= require folio/console/files/show_modal_component
Expand Down
46 changes: 46 additions & 0 deletions app/components/folio/console/files/show/encoding_info_component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
window.Folio.Stimulus.register('f-c-files-show-encoding-info', class extends window.Stimulus.Controller {
static values = {
fileId: Number
}

connect () {
this.messageBusCallbackKey = `f-c-files-show-encoding-info--${this.fileIdValue}`
window.Folio.MessageBus.callbacks[this.messageBusCallbackKey] = (message) => {
if (message.type === 'Folio::CraMediaCloud::CheckProgressJob/encoding_progress' &&
message.data.id === this.fileIdValue) {
this.update(message.data)
}
}
}

disconnect () {
if (this.messageBusCallbackKey && window.Folio.MessageBus.callbacks) {
delete window.Folio.MessageBus.callbacks[this.messageBusCallbackKey]
}
}

update (data) {
const phaseEl = this.element.querySelector('.f-c-files-show-encoding-info__phase')
const progressEl = this.element.querySelector('.f-c-files-show-encoding-info__progress')

if (data.aasm_state === 'processing_failed') {
if (phaseEl) {
phaseEl.classList.add('f-c-files-show-encoding-info__phase--failed')
phaseEl.textContent = data.failed_label || ''
}
if (progressEl) {
progressEl.textContent = ''
}
return
}

if (phaseEl && data.current_phase_label) {
phaseEl.classList.remove('f-c-files-show-encoding-info__phase--failed')
phaseEl.textContent = data.current_phase_label
}

if (progressEl) {
progressEl.textContent = data.progress_percentage != null ? `${data.progress_percentage}%` : ''
}
}
})
74 changes: 74 additions & 0 deletions app/components/folio/console/files/show/encoding_info_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

class Folio::Console::Files::Show::EncodingInfoComponent < Folio::Console::ApplicationComponent
def initialize(file:)
@file = file
@rsd = file.remote_services_data || {}
end

def render?
cra_file? && (processing? || failed?)
end

def processing?
@file.processing?
end

def failed?
@file.processing_failed?
end

def retrying?
failed? && @rsd["retry_scheduled_at"].present? && @rsd["retry_count"].to_i < 2
end

def current_phase
@rsd["current_phase"]
end

def current_phase_label
return current_phase&.humanize if current_phase.blank?

encoding_phase = @rsd["current_encoding_phase"]
processing_phases = @rsd["processing_phases"].to_i

if processing_phases > 1 && encoding_phase.present?
phase_name = @file.try(:encoder_phase_name, encoding_phase)
if phase_name
t(".phase_#{current_phase}_named",
name: phase_name,
default: t(".phase_#{current_phase}", default: current_phase.humanize))
else
t(".phase_#{current_phase}_multi",
phase: encoding_phase,
total: processing_phases,
default: t(".phase_#{current_phase}", default: current_phase.humanize))
end
else
t(".phase_#{current_phase}", default: current_phase.humanize)
end
end

def encoding_progress
@rsd["progress_percentage"]
end

def data
{
"controller" => "f-c-files-show-encoding-info",
"f-c-files-show-encoding-info-file-id-value" => @file.id,
}
end

private
def cra_file?
# Check capability first (covers enqueued state before 'service' is written)
return true if @file.is_a?(Folio::CraMediaCloud::FileProcessing)

# Fallback for plain Folio::File::Video without concern (legacy or plain video)
@file.try(:processing_service) == "cra_media_cloud" ||
@rsd["service"] == "cra_media_cloud" ||
@rsd["current_phase"].present? ||
@rsd["retry_count"].present?
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.f-c-files-show-encoding-info
display: inline
color: $gray-600
font-size: $font-size-sm
white-space: nowrap

&:empty
display: none

&__progress
&:not(:empty)::before
content: " · "

&__phase--failed
color: $danger

// Pulse the yellow state dot when encoding info component follows the state cell
.f-c-files-show__meta-item:has(+ .f-c-files-show-encoding-info:not(:empty))
.f-c-state__state-square--state-processing
animation: f-c-files-show-encoding-info-pulse 2s ease-in-out infinite

@keyframes f-c-files-show-encoding-info-pulse
0%, 100%
opacity: 1
50%
opacity: 0.35
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
span.f-c-files-show-encoding-info data=data
- if failed?
span.f-c-files-show-encoding-info__phase.f-c-files-show-encoding-info__phase--failed
- if retrying?
= t(".phase_failed_retrying")
- else
= t(".phase_failed")
- elsif processing?
span.f-c-files-show-encoding-info__phase
= current_phase_label
span.f-c-files-show-encoding-info__progress
- if encoding_progress.present?
= "#{encoding_progress}%"
56 changes: 54 additions & 2 deletions app/components/folio/console/files/show_component.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ window.Folio.Stimulus.register('f-c-files-show', class extends window.Stimulus.C
fileType: String,
id: String,
showUrl: String,
indexUrl: String
indexUrl: String,
aasmState: String
}

disconnect () {
Expand Down Expand Up @@ -93,6 +94,15 @@ window.Folio.Stimulus.register('f-c-files-show', class extends window.Stimulus.C

messageBusCallback (event) {
const message = event.detail.message

if (message.type === 'Folio::CraMediaCloud::CheckProgressJob/encoding_progress') {
return this.handleEncodingProgress(message.data)
}

if (message.type === 'Folio::ApplicationJob/file_update') {
return this.handleFileUpdate(message.data)
}

if (message.type !== 'Folio::S3::CreateFileJob') return
switch (message.data.type) {
case 'replace-success':
Expand All @@ -104,10 +114,34 @@ window.Folio.Stimulus.register('f-c-files-show', class extends window.Stimulus.C
}
}

messageBusSuccess (data) {
handleEncodingProgress (data) {
if (data.aasm_state === 'processing') {
// Update state badge label
const stateLabel = this.element.querySelector('.f-c-state__state-label')
if (stateLabel) stateLabel.textContent = data.aasm_state_human
} else {
// Encoding finished or failed — reload to show final state
this.reloadFrame()
}
}

handleFileUpdate (data) {
if (!data || !data.attributes) return

const newState = data.attributes.aasm_state
if (newState && newState !== this.aasmStateValue) {
this.reloadFrame()
}
}

reloadFrame () {
window.Turbo.visit(this.showUrlValue, { frame: this.element.closest('turbo-frame').id })
}

messageBusSuccess (data) {
this.reloadFrame()
}

messageBusFailure (data) {
this.loadingValue = false
delete this.replacingFileData
Expand All @@ -128,6 +162,24 @@ if (window.Folio && window.Folio.MessageBus && window.Folio.MessageBus.callbacks
}
}

if (message.type === 'Folio::CraMediaCloud::CheckProgressJob/encoding_progress') {
const selector = `.f-c-files-show[data-f-c-files-show-id-value="${message.data.id}"]`
const targets = document.querySelectorAll(selector)

for (const target of targets) {
target.dispatchEvent(new CustomEvent('f-c-files-show/message', { detail: { message } }))
}
}

if (message.type === 'Folio::ApplicationJob/file_update') {
const selector = `.f-c-files-show[data-f-c-files-show-id-value="${message.data.id}"]`
const targets = document.querySelectorAll(selector)

for (const target of targets) {
target.dispatchEvent(new CustomEvent('f-c-files-show/message', { detail: { message } }))
}
}

if (message.type === 'f-c-files-show:reload') {
const selector = `.f-c-files-show[data-f-c-files-show-id-value="${message.data.id}"]`
const targets = document.querySelectorAll(selector)
Expand Down
3 changes: 2 additions & 1 deletion app/components/folio/console/files/show_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def data
id: @file.id,
file_type: @file.class.to_s,
show_url: controller.folio.url_for([:console, @file]),
index_url: controller.folio.url_for([:console, @file.class])
index_url: controller.folio.url_for([:console, @file.class]),
aasm_state: @file.aasm_state
},
action: {
"f-uppy:upload-success": "uppyUploadSuccess",
Expand Down
6 changes: 4 additions & 2 deletions app/components/folio/console/files/show_component.slim
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@
= @file.file_mime_type

- if @file.created_at.present?
.f-c-files-show__meta-item.me-auto
.f-c-files-show__meta-item
' #{t(".created_at")}: #{l(@file.created_at.to_date, format: :console_short)}

.f-c-files-show__meta-item
.f-c-files-show__meta-item.ms-auto
== cell("folio/console/state", @file, active: false)

= render(Folio::Console::Files::Show::EncodingInfoComponent.new(file: @file))

.f-c-files-show__table
- table_rows.each do |key, config|
.f-c-files-show__tr
Expand Down
Loading