Class: VideoChapter

Inherits:
ApplicationRecord show all
Defined in:
app/models/video_chapter.rb

Overview

A single YouTube-style chapter marker for a Video. Persisted locally so we
can edit and re-push to the YouTube description without re-running AI
generation.

Constant Summary

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#start_msvoid (readonly)

This method returns an undefined value.



17
# File 'app/models/video_chapter.rb', line 17

validates :start_ms, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }

#titlevoid (readonly)

This method returns an undefined value.



15
# File 'app/models/video_chapter.rb', line 15

validates :title, presence: true, length: { maximum: 100 }

Class Method Details

.format_ms_to_hms(ms) ⇒ String?

Format a millisecond offset for display. Drops the hour part when zero.

Parameters:

  • ms (Integer, nil)

Returns:

  • (String, nil)


76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/models/video_chapter.rb', line 76

def self.format_ms_to_hms(ms)
  return nil if ms.nil?

  total_seconds = ms.to_i / 1000
  hours = total_seconds / 3600
  minutes = (total_seconds % 3600) / 60
  seconds = total_seconds % 60
  if hours.positive?
    format('%<h>d:%<m>02d:%<s>02d', h: hours, m: minutes, s: seconds)
  else
    format('%<m>d:%<s>02d', m: minutes, s: seconds)
  end
end

.parse_timestamp_to_ms(value) ⇒ Integer?

Parse a user-entered timestamp into milliseconds.

Parameters:

  • value (String, Integer, nil)

Returns:

  • (Integer, nil)

    milliseconds, or nil when the input is blank or unparseable.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/models/video_chapter.rb', line 56

def self.parse_timestamp_to_ms(value)
  return nil if value.nil?

  s = value.to_s.strip
  return nil if s.empty?

  parts = s.split(':')
  return nil unless parts.all? { |p| p.match?(/\A\d+\z/) }

  nums = parts.map(&:to_i)
  case nums.length
  when 1 then nums[0] * 1000
  when 2 then ((nums[0] * 60) + nums[1]) * 1000
  when 3 then ((nums[0] * 3600) + (nums[1] * 60) + nums[2]) * 1000
  end
end

Instance Method Details

#start_within_video_durationvoid

This method returns an undefined value.

The authoritative out-of-bounds guard: a chapter must start strictly before
the video ends. A marker at or past the duration is invalid both as a
YouTube chapter and as a VideoObject Clip (startOffset must be < duration),
and it strands the final Clip without an endOffset. Enforced at the model
so no edit path — CRM form, AI generation, YouTube pull, console — can
persist one. Skipped when the duration is unknown (nothing to bound against);
the generation-time clamp and the presenter still defend the render side.



29
30
31
32
33
34
35
36
37
# File 'app/models/video_chapter.rb', line 29

def start_within_video_duration
  return if start_ms.blank?

  duration_ms = video&.duration_ms || 0
  return unless duration_ms.positive?
  return if start_ms < duration_ms

  errors.add(:start_ms, "must be before the video ends (#{self.class.format_ms_to_hms(duration_ms)})")
end

#timestamp_hmsString?

Hours-minutes-seconds string (zero-padded MM/SS), used by the CRM form.

Returns:

  • (String, nil)

    e.g. "1:02:03" or "0:42"; nil when start_ms is blank.



41
42
43
# File 'app/models/video_chapter.rb', line 41

def timestamp_hms
  self.class.format_ms_to_hms(start_ms)
end

#timestamp_hms=(value) ⇒ Integer?

Setter paired with #timestamp_hms for accepts_nested_attributes_for forms.
Accepts "H:MM:SS", "M:SS", or a bare integer seconds string. Blank clears.

Parameters:

  • value (String, Integer, nil)

Returns:

  • (Integer, nil)

    the milliseconds value just assigned to start_ms.



49
50
51
# File 'app/models/video_chapter.rb', line 49

def timestamp_hms=(value)
  self.start_ms = self.class.parse_timestamp_to_ms(value)
end

#videoVideo

Returns:



10
# File 'app/models/video_chapter.rb', line 10

belongs_to :video, foreign_key: :digital_asset_id, class_name: 'Video', inverse_of: :video_chapters