-
+
-
- <%= message.presence %>
<% if show_percentage %>
- ...
- <%= number_to_percentage completed, precision: 2 %>
- <% if eta.present? %>
- (<%= eta %>)
- <% end %>
+
+ <%= number_to_percentage completed, precision: 2 %>
+
<% end %>
+
+ <%= message.presence&.html_safe %>
+
+ <% if eta.present? %>
+
ETA: <%= eta %>
+ <% end %>
+ <% if progress.present? %>
+
Progress: <%= progress %>
+ <% end %>
diff --git a/app/components/progress_bar_component.rb b/app/components/progress_bar_component.rb
index 30f227af..c126686e 100644
--- a/app/components/progress_bar_component.rb
+++ b/app/components/progress_bar_component.rb
@@ -7,4 +7,5 @@ class ProgressBarComponent < ViewComponent::Base
option :message, optional: true
option :show_percentage, default: -> { true }
option :eta, optional: true
+ option :progress, optional: true
end
diff --git a/app/components/rip_process_component.html.erb b/app/components/rip_process_component.html.erb
index 9adb7897..7209ea54 100644
--- a/app/components/rip_process_component.html.erb
+++ b/app/components/rip_process_component.html.erb
@@ -13,7 +13,7 @@
completed: job.metadata['completed'],
status: :info,
message: job.metadata['title'],
- eta:
+ eta: job.metadata['eta']
)
)
%>
diff --git a/app/components/upload_process_component.html.erb b/app/components/upload_process_component.html.erb
index 981a6f33..ef45e9fe 100644
--- a/app/components/upload_process_component.html.erb
+++ b/app/components/upload_process_component.html.erb
@@ -14,12 +14,17 @@
ProgressBarComponent.new(
show_percentage: video_blob_job.present?,
status: :info,
- completed: percentage(video_blob_job&.completed, blob.byte_size),
- message: video_blob_job ? "Uploading #{blob.title}" : "Pending upload #{blob.title} ##{blob.id}",
- eta: (eta(video_blob_job, blob) if video_blob_job)
+ completed: video_blob_job&.completed,
+ message: video_blob_job ? "Uploading #{link_to_video(blob)}" : "Pending #{link_to_video(blob)} ##{blob.id}",
+ eta: video_blob_job&.metadata&.fetch('eta', nil),
+ progress: [
+ video_blob_job&.metadata&.fetch('progress', nil),
+ video_blob_job&.metadata&.fetch('rate', nil)
+ ].compact_blank.join(' ')
)
)
%>
+
<% end %>
<% uploaded_recently_video_blobs.each do |blob| %>
@@ -29,7 +34,7 @@
show_percentage: false,
status: :success,
completed: 100,
- message: "Uploaded #{blob.title}"
+ message: "Uploaded #{link_to_video(blob)}"
)
)
%>
diff --git a/app/components/upload_process_component.rb b/app/components/upload_process_component.rb
index 69b085bb..a49c6626 100644
--- a/app/components/upload_process_component.rb
+++ b/app/components/upload_process_component.rb
@@ -31,6 +31,14 @@ def uploaded_recently_video_blobs
.limit(3)
end
+ def link_to_video(blob)
+ return link_to(blob.title, movie_path(blob.video)) if blob.video.is_a?(Movie)
+
+ return link_to(blob.title, tv_season_path(blob.video, blob.episode.season)) if blob.episode && blob.video.is_a?(Tv)
+
+ blob.title
+ end
+
def job_active?
job&.active?
end
diff --git a/app/listeners/mkv_progress_listener.rb b/app/listeners/mkv_progress_listener.rb
index b8ad896e..a0ac098c 100644
--- a/app/listeners/mkv_progress_listener.rb
+++ b/app/listeners/mkv_progress_listener.rb
@@ -61,8 +61,12 @@ def mkv_started(cmd)
def mkv_raw_line(mkv_message)
case mkv_message
when MkvParser::PRGV
- job.completed = percentage(mkv_message.current, mkv_message.pmax)
+ tracker(mkv_message.pmax)
+ increment_progress_bar(mkv_message.current)
+ job.completed = tracker.percentage_component.percentage_with_precision
+ job.metadata['eta'] = tracker.time_component.estimated_without_label
when MkvParser::PRGT, MkvParser::PRGC
+ @tracker = nil
job.title = [video_blob&.title, mkv_message.name].compact_blank.join("\n")
when MkvParser::MSG
job.add_message(mkv_message.message)
@@ -74,6 +78,19 @@ def mkv_raw_line(mkv_message)
private
+ def tracker(total = nil)
+ @tracker = nil if total && @tracker&.total != total
+ @tracker ||= ProgressTracker::Base.new(total:)
+ end
+
+ def increment_progress_bar(progress)
+ return if tracker.finished?
+
+ tracker.progress = progress
+ rescue ProgressBar::InvalidProgressError
+ tracker&.finish
+ end
+
def reload_page!
cable_ready[BroadcastChannel.channel_name].reload
cable_ready.broadcast
diff --git a/app/listeners/upload_progress_listener.rb b/app/listeners/upload_progress_listener.rb
index 0462d289..2cb05fe8 100644
--- a/app/listeners/upload_progress_listener.rb
+++ b/app/listeners/upload_progress_listener.rb
@@ -12,8 +12,8 @@ class UploadProgressListener
attr_reader :completed
- def upload_progress(total_uploaded:)
- job.completed = total_uploaded
+ def upload_progress(tracker:)
+ update_meta_from_tracker(tracker)
return if next_update.future?
job.save!
@@ -25,15 +25,15 @@ def upload_ready
upload_started
end
- def upload_started
- job.completed = 0
+ def upload_started(tracker: nil)
+ update_meta_from_tracker(tracker)
job.metadata['video_blob_id'] = video_blob.id
job.save!
update_component
end
- def upload_finished
- job.completed = video_blob.byte_size
+ def upload_finished(tracker:)
+ update_meta_from_tracker(tracker)
job.save!
video_blob.update!(uploadable: false, uploaded_on: Time.current)
update_component
@@ -46,6 +46,15 @@ def upload_error(exception)
private
+ def update_meta_from_tracker(tracker)
+ return if tracker.nil?
+
+ job.completed = tracker.percentage_component.percentage_with_precision
+ job.metadata['eta'] = tracker.time_component.estimated_without_label
+ job.metadata['rate'] = "#{tracker.rate_component.rate_of_change_with_precision} KB/sec"
+ job.metadata['progress'] = "#{tracker.progress} KB"
+ end
+
def update_component
component = UploadProcessComponent.new
cable_ready[BroadcastChannel.channel_name].morph \
diff --git a/app/services/ftp/upload_mkv_service.rb b/app/services/ftp/upload_mkv_service.rb
index 26f0cfca..08f644fa 100644
--- a/app/services/ftp/upload_mkv_service.rb
+++ b/app/services/ftp/upload_mkv_service.rb
@@ -14,7 +14,7 @@ def call
try_to { ftp_upload_file }
tmp_destroy_folder
mark_as_uploaded!
- broadcast(:upload_finished)
+ broadcast(:upload_finished, tracker:)
rescue StandardError => e
broadcast(:upload_error, e)
raise e
@@ -22,6 +22,8 @@ def call
private
+ attr_reader :tracker
+
def ftp_destroy_if_file_exists
ftp.delete(video_blob.plex_path)
rescue Net::FTPPermError => e
@@ -41,14 +43,31 @@ def ftp_create_directory
end
def ftp_upload_file
- broadcast(:upload_started)
- total_uploaded = 0
+ @tracker = ProgressTracker::Base.new(
+ total: bytes_to_kilobyte(file.size),
+ rate_scale: ->(rate) { rate / 1024 }
+ )
+ broadcast(:upload_started, tracker:)
ftp.putbinaryfile(file, video_blob.plex_path) do |chunk|
- total_uploaded += chunk.size
- broadcast(:upload_progress, total_uploaded:)
+ increment_progress_bar(chunk.size)
+ broadcast(:upload_progress, tracker:)
end
end
+ def increment_progress_bar(increment)
+ return if tracker.finished?
+
+ @total_bytes ||= 0
+ @total_bytes += increment
+ tracker.progress = bytes_to_kilobyte(@total_bytes)
+ rescue ProgressBar::InvalidProgressError
+ tracker&.finish
+ end
+
+ def bytes_to_kilobyte(bites)
+ bites / 1000
+ end
+
def mark_as_uploaded!
video_blob.update!(uploaded_on: Time.current, uploadable: false)
end
diff --git a/config/application.rb b/config/application.rb
index 1fde7ea1..3d29961f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -18,6 +18,7 @@
Bundler.require(*Rails.groups)
require 'view_component/engine'
require 'sys/filesystem'
+require './lib/progress_tracker/base'
module PlexRipper
VERSION = File.read(File.expand_path('./current_version.txt')).gsub('v', '').strip
diff --git a/current_version.txt b/current_version.txt
index cce56663..268fccb1 100644
--- a/current_version.txt
+++ b/current_version.txt
@@ -1 +1 @@
-v5.4.1
+v5.5.0
diff --git a/lib/progress_tracker/base.rb b/lib/progress_tracker/base.rb
new file mode 100644
index 00000000..cf7155d1
--- /dev/null
+++ b/lib/progress_tracker/base.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require_relative 'time'
+require_relative 'timer'
+require_relative 'progress'
+require_relative 'projector'
+require_relative 'projectors/smoothed_average'
+require_relative 'component/rate'
+require_relative 'component/time'
+require_relative 'component/percentage'
+
+module ProgressTracker
+ class Base
+ extend Forwardable
+
+ def_delegators :progressable,
+ :progress,
+ :total
+ def initialize(options = {})
+ options[:projector] ||= {}
+
+ self.autostart = options.fetch(:autostart, true)
+ self.autofinish = options.fetch(:autofinish, true)
+ self.finished = false
+
+ self.timer = Timer.new(options)
+ self.projector = Projector
+ .from_type(options[:projector][:type])
+ .new(options[:projector])
+ self.progressable = Progress.new(options)
+
+ options = options.merge(progress: progressable,
+ projector:,
+ timer:)
+
+ self.percentage_component = Components::Percentage.new(options)
+ self.rate_component = Components::Rate.new(options)
+ self.time_component = Components::Time.new(options)
+
+ start at: options[:starting_at] if autostart
+ end
+
+ def start(options = {})
+ timer.start
+ update_progress(:start, options)
+ end
+
+ def finish
+ return if finished?
+
+ output.with_refresh do
+ self.finished = true
+ progressable.finish
+ timer.stop
+ end
+ end
+
+ def pause
+ output.with_refresh { timer.pause } unless paused?
+ end
+
+ def stop
+ output.with_refresh { timer.stop } unless stopped?
+ end
+
+ def resume
+ output.with_refresh { timer.resume } if stopped?
+ end
+
+ def reset
+ output.with_refresh do
+ self.finished = false
+ progressable.reset
+ projector.reset
+ timer.reset
+ end
+ end
+
+ def stopped?
+ timer.stopped? || finished?
+ end
+
+ alias paused? stopped?
+
+ def finished?
+ finished || (autofinish && progressable.finished?)
+ end
+
+ def started? # rubocop:disable Rails/Delegate
+ timer.started?
+ end
+
+ def decrement
+ update_progress(:decrement)
+ end
+
+ def increment
+ update_progress(:increment)
+ end
+
+ def progress=(new_progress)
+ update_progress(:progress=, new_progress)
+ end
+
+ def total=(new_total)
+ update_progress(:total=, new_total)
+ end
+
+ def inspect
+ "#
"
+ end
+
+ attr_accessor :percentage_component,
+ :rate_component,
+ :time_component
+
+ protected
+
+ attr_accessor :autofinish,
+ :autostart,
+ :finished,
+ :progressable,
+ :projector,
+ :timer
+
+ def update_progress(*)
+ progressable.__send__(*)
+ projector.__send__(*)
+ timer.stop if finished?
+ end
+ end
+end
diff --git a/lib/progress_tracker/component/percentage.rb b/lib/progress_tracker/component/percentage.rb
new file mode 100644
index 00000000..02cfb06a
--- /dev/null
+++ b/lib/progress_tracker/component/percentage.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ module Components
+ class Percentage
+ attr_accessor :progress
+
+ def initialize(options = {})
+ self.progress = options[:progress]
+ end
+
+ def percentage
+ progress.percentage_completed.to_s
+ end
+
+ def justified_percentage
+ progress.percentage_completed.to_s.rjust(3)
+ end
+
+ def percentage_with_precision
+ progress.percentage_completed_with_precision
+ end
+
+ def justified_percentage_with_precision
+ progress.percentage_completed_with_precision.to_s.rjust(6)
+ end
+ end
+ end
+end
diff --git a/lib/progress_tracker/component/rate.rb b/lib/progress_tracker/component/rate.rb
new file mode 100644
index 00000000..84a404aa
--- /dev/null
+++ b/lib/progress_tracker/component/rate.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ module Components
+ class Rate
+ attr_accessor :rate_scale,
+ :timer,
+ :progress
+
+ def initialize(options = {})
+ self.rate_scale = options[:rate_scale] || ->(x) { x }
+ self.timer = options[:timer]
+ self.progress = options[:progress]
+ end
+
+ def rate_of_change(format_string = '%i')
+ return '0' if elapsed_seconds <= 0
+
+ format_string % scaled_rate
+ end
+
+ def rate_of_change_with_precision
+ rate_of_change('%.2f')
+ end
+
+ private
+
+ def scaled_rate
+ rate_scale.call(base_rate)
+ end
+
+ def base_rate
+ progress.absolute / elapsed_seconds
+ end
+
+ def elapsed_seconds
+ timer.elapsed_whole_seconds.to_f
+ end
+ end
+ end
+end
diff --git a/lib/progress_tracker/component/time.rb b/lib/progress_tracker/component/time.rb
new file mode 100644
index 00000000..d851335a
--- /dev/null
+++ b/lib/progress_tracker/component/time.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ module Components
+ class Time
+ TIME_FORMAT = '%02d:%02d:%02d'
+ OOB_TIME_FORMATS = [:unknown, :friendly, nil].freeze
+ OOB_LIMIT_IN_HOURS = 99
+ OOB_UNKNOWN_TIME_TEXT = '??:??:??'
+ OOB_FRIENDLY_TIME_TEXT = '> 4 Days'
+ NO_TIME_ELAPSED_TEXT = '--:--:--'
+ ESTIMATED_LABEL = ' ETA'
+ ELAPSED_LABEL = 'Time'
+ WALL_CLOCK_FORMAT = '%H:%M:%S'
+ OOB_TEXT_TO_FORMAT = {
+ unknown: OOB_UNKNOWN_TIME_TEXT,
+ friendly: OOB_FRIENDLY_TIME_TEXT
+ }.freeze
+
+ def initialize(options = {})
+ self.timer = options[:timer]
+ self.progress = options[:progress]
+ self.projector = options[:projector]
+ end
+
+ def estimated_without_label(out_of_bounds_time_format = nil)
+ estimated(out_of_bounds_time_format)
+ end
+
+ def estimated_with_label(out_of_bounds_time_format = nil)
+ "#{ESTIMATED_LABEL}: #{estimated(out_of_bounds_time_format)}"
+ end
+
+ def elapsed_with_label
+ "#{ELAPSED_LABEL}: #{elapsed}"
+ end
+
+ def estimated_with_no_oob
+ estimated_with_elapsed_fallback(nil)
+ end
+
+ def estimated_with_unknown_oob
+ estimated_with_elapsed_fallback(:unknown)
+ end
+
+ def estimated_with_friendly_oob
+ estimated_with_elapsed_fallback(:friendly)
+ end
+
+ def estimated_wall_clock
+ return timer.stopped_at.strftime(WALL_CLOCK_FORMAT) if progress.finished?
+ return NO_TIME_ELAPSED_TEXT unless timer.started?
+
+ memo_estimated_seconds_remaining = estimated_seconds_remaining
+ return NO_TIME_ELAPSED_TEXT unless memo_estimated_seconds_remaining
+
+ (timer.now + memo_estimated_seconds_remaining)
+ .strftime(WALL_CLOCK_FORMAT)
+ end
+
+ protected
+
+ attr_accessor :timer,
+ :progress,
+ :projector
+
+ private
+
+ def estimated(out_of_bounds_time_format)
+ memo_estimated_seconds_remaining = estimated_seconds_remaining
+
+ return OOB_UNKNOWN_TIME_TEXT unless memo_estimated_seconds_remaining
+
+ hours, minutes, seconds = timer.divide_seconds(memo_estimated_seconds_remaining)
+
+ if hours > OOB_LIMIT_IN_HOURS && out_of_bounds_time_format
+ OOB_TEXT_TO_FORMAT.fetch(out_of_bounds_time_format)
+ else
+ format(TIME_FORMAT, hours, minutes, seconds)
+ end
+ end
+
+ def elapsed
+ return NO_TIME_ELAPSED_TEXT unless timer.started?
+
+ hours, minutes, seconds = timer.divide_seconds(timer.elapsed_whole_seconds)
+
+ format(TIME_FORMAT, hours, minutes, seconds)
+ end
+
+ def estimated_with_elapsed_fallback(out_of_bounds_time_format)
+ return elapsed_with_label if progress.finished?
+
+ estimated_with_label(out_of_bounds_time_format)
+ end
+
+ def estimated_seconds_remaining
+ return if progress.unknown? || projector.none? || progress.none? || timer.stopped? || timer.reset?
+
+ (timer.elapsed_seconds * ((progress.total / projector.projection) - 1)).round
+ end
+ end
+ end
+end
diff --git a/lib/progress_tracker/progress.rb b/lib/progress_tracker/progress.rb
new file mode 100644
index 00000000..80861247
--- /dev/null
+++ b/lib/progress_tracker/progress.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'ruby-progressbar/errors/invalid_progress_error'
+
+module ProgressTracker
+ class Progress
+ DEFAULT_TOTAL = 100
+ DEFAULT_BEGINNING_POSITION = 0
+
+ attr_reader :total,
+ :progress
+ attr_accessor :starting_position
+
+ def initialize(options = {})
+ self.total = options.fetch(:total, DEFAULT_TOTAL)
+
+ start(at: DEFAULT_BEGINNING_POSITION)
+ end
+
+ def start(options = {})
+ self.progress =
+ self.starting_position = options[:at] || progress
+ end
+
+ def finish
+ self.progress = total unless unknown?
+ end
+
+ def finished?
+ @progress == @total
+ end
+
+ def increment
+ warn "WARNING: Your progress bar is currently at #{progress} out of #{total}" if progress == total
+
+ self.progress += 1 unless progress == total
+ end
+
+ def decrement
+ warn "WARNING: Your progress bar is currently at #{progress} out of #{total}" if progress.zero?
+
+ self.progress -= 1 unless progress.zero?
+ end
+
+ def reset
+ start(at: starting_position)
+ end
+
+ def progress=(new_progress)
+ if total && new_progress > total
+ raise ProgressBar::InvalidProgressError,
+ "You can't set the item's current value to be greater than the total."
+ end
+
+ @progress = new_progress
+ end
+
+ def total=(new_total)
+ unless progress.nil? || new_total.nil? || new_total >= progress
+ raise ProgressBar::InvalidProgressError,
+ "You can't set the item's total value to less than the current progress."
+ end
+
+ @total = new_total
+ end
+
+ def percentage_completed
+ return 0 if total.nil?
+ return 100 if total.zero?
+
+ # progress / total * 100
+ #
+ # Doing this way so we can avoid converting each
+ # number to a float and then back to an integer.
+ #
+ (progress * 100 / total).to_i
+ end
+
+ def none?
+ progress.zero?
+ end
+
+ def unknown?
+ progress.nil? || total.nil?
+ end
+
+ def total_with_unknown_indicator
+ total || '??'
+ end
+
+ def percentage_completed_with_precision
+ return 100.0 if total.zero?
+ return 0.0 if total.nil?
+
+ format('%5.2f', (progress * 100 / total.to_f * 100).floor / 100.0)
+ end
+
+ def absolute
+ progress - starting_position
+ end
+ end
+end
diff --git a/lib/progress_tracker/projector.rb b/lib/progress_tracker/projector.rb
new file mode 100644
index 00000000..48b2ffa8
--- /dev/null
+++ b/lib/progress_tracker/projector.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'ruby-progressbar/projectors/smoothed_average'
+
+module ProgressTracker
+ class Projector
+ DEFAULT_PROJECTOR = ProgressBar::Projectors::SmoothedAverage
+ NAME_TO_PROJECTOR_MAP = {
+ 'smoothed' => ProgressBar::Projectors::SmoothedAverage
+ }.freeze
+
+ def self.from_type(name)
+ NAME_TO_PROJECTOR_MAP.fetch(name, DEFAULT_PROJECTOR)
+ end
+ end
+end
diff --git a/lib/progress_tracker/projectors/smoothed_average.rb b/lib/progress_tracker/projectors/smoothed_average.rb
new file mode 100644
index 00000000..7d70a7aa
--- /dev/null
+++ b/lib/progress_tracker/projectors/smoothed_average.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ module Projectors
+ class SmoothedAverage
+ DEFAULT_STRENGTH = 0.1
+ DEFAULT_BEGINNING_POSITION = 0
+
+ attr_accessor :samples,
+ :strength
+ attr_reader :projection
+
+ def initialize(options = {})
+ self.samples = []
+ self.projection = 0.0
+ self.strength = options[:strength] || DEFAULT_STRENGTH
+
+ start(at: DEFAULT_BEGINNING_POSITION)
+ end
+
+ def start(options = {})
+ self.projection = 0.0
+ self.progress = samples[0] = (options[:at] || progress)
+ end
+
+ def decrement
+ self.progress -= 1
+ end
+
+ def increment
+ self.progress += 1
+ end
+
+ def progress
+ samples[1]
+ end
+
+ def total=(_new_total); end
+
+ def reset
+ start(at: samples[0])
+ end
+
+ def progress=(new_progress)
+ samples[1] = new_progress
+ self.projection =
+ self.class.calculate(
+ @projection,
+ absolute,
+ strength
+ )
+ end
+
+ def none?
+ projection.zero?
+ end
+
+ def self.calculate(current_projection, new_value, rate)
+ (new_value * (1.0 - rate)) + (current_projection * rate)
+ end
+
+ protected
+
+ attr_writer :projection
+
+ private
+
+ def absolute
+ samples[1] - samples[0]
+ end
+ end
+ end
+end
diff --git a/lib/progress_tracker/time.rb b/lib/progress_tracker/time.rb
new file mode 100644
index 00000000..0240a652
--- /dev/null
+++ b/lib/progress_tracker/time.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ class Time
+ TIME_MOCKING_LIBRARY_METHODS = [
+ :__simple_stub__now, # ActiveSupport
+ :now_without_mock_time, # Timecop
+ :now_without_delorean, # Delorean
+ :now # Unmocked
+ ].freeze
+
+ def initialize(time = ::Time)
+ self.time = time
+ end
+
+ def now
+ time.__send__(unmocked_time_method)
+ end
+
+ def unmocked_time_method
+ @unmocked_time_method ||= TIME_MOCKING_LIBRARY_METHODS.find do |method|
+ time.respond_to? method
+ end
+ end
+
+ protected
+
+ attr_accessor :time
+ end
+end
+# rubocop:enable Style/InlineComment
diff --git a/lib/progress_tracker/timer.rb b/lib/progress_tracker/timer.rb
new file mode 100644
index 00000000..c50f61a1
--- /dev/null
+++ b/lib/progress_tracker/timer.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module ProgressTracker
+ class Timer
+ attr_accessor :started_at,
+ :stopped_at
+
+ def initialize(options = {})
+ self.time = options[:time] || ::ProgressTracker::Time.new
+ end
+
+ def start
+ self.started_at = stopped? ? time.now - (stopped_at - started_at) : time.now
+ self.stopped_at = nil
+ end
+
+ def stop
+ return unless started?
+
+ self.stopped_at = time.now
+ end
+
+ def pause
+ stop
+ end
+
+ def resume
+ start
+ end
+
+ delegate :now, to: :time
+
+ def started?
+ started_at
+ end
+
+ def stopped?
+ stopped_at
+ end
+
+ def reset
+ self.started_at = nil
+ self.stopped_at = nil
+ end
+
+ def reset?
+ !started_at
+ end
+
+ def restart
+ reset
+ start
+ end
+
+ def elapsed_seconds
+ return 0 unless started?
+
+ ((stopped_at || time.now) - started_at)
+ end
+
+ def elapsed_whole_seconds
+ elapsed_seconds.floor
+ end
+
+ def divide_seconds(seconds)
+ hours, seconds = seconds.divmod(3600)
+ minutes, seconds = seconds.divmod(60)
+
+ [hours, minutes, seconds]
+ end
+
+ protected
+
+ attr_accessor :time
+ end
+end
diff --git a/spec/listeners/upload_progress_listener_spec.rb b/spec/listeners/upload_progress_listener_spec.rb
index e5d6b418..77d5f19a 100644
--- a/spec/listeners/upload_progress_listener_spec.rb
+++ b/spec/listeners/upload_progress_listener_spec.rb
@@ -6,7 +6,9 @@
subject(:listener) { described_class.new(**args) }
describe '#upload_progress' do
- subject(:upload_progress) { listener.upload_progress(total_uploaded: 10) }
+ subject(:upload_progress) { listener.upload_progress(tracker:) }
+
+ let(:tracker) { ProgressTracker::Base.new }
let(:video_blob) { build_stubbed(:video_blob) }
let(:job) { build(:job) }
@@ -21,7 +23,12 @@
it 'changes the completed size based on the chunk size' do
upload_progress
- expect(listener.job.metadata).to eq('completed' => 10)
+ expect(listener.job.metadata).to eq({
+ 'completed' => 0.0,
+ 'eta' => '??:??:??',
+ 'rate' => '0 KB/sec',
+ 'progress' => '0 KB'
+ })
end
end
end