class SubtitleShifter

@example Manipulate subtitles from the command line

$ subtitle_shifter --help
$ subtitle_shifter --operation add --index 12 --time 2,345 source.srt dest.srt

@example Manipulate subtitles from within a ruby program

# This will shift all subtitles from index 12 onward by 2.345 seconds
# or 2345 milliseconds
subs = SubtitleShifter.new('mysubs.srt')
subs.parse
subs.shift(:index => 12, :time => 2345)

@example Shift subtitles backward

# This will shift subtitles backward, beware - you cannot shift
# subtitles backward so that they overlap the preceding subtitles.
# A RuntimeError exception will be raised if this occurs.
subs.shift(:index => 12, :time => -2345) # Simply provide a negative time value

@example Output subtitles once they've been parsed and/or manipulated

puts subs
# -- or --
subs.to_s

@see en.wikipedia.org/wiki/Subrip Wikipedia's article on the SubRip format

Constants

TIME_SEPERATOR

The delimiter used for separating SubRip time stamps

Attributes

parsed_ok[R]

A boolean flag highlighting whether or not subtitles have been parsed yet

subtitles[R]

A hash of the parsed subtitles. You normally wouldn't need to access this directly @example The format of the hash is as follows

{1 => {:start => 107,
       :end   => 5762,
       :subtitle => 'This is the first subtitle'
      },
{2 => {:start => 5890,
       :end   => 10553,
       :subtitle => 'This is the second subtitle'
      }

@note I chose to implement internal representation of subtitle files as a hash

and not an array, which would've been more efficient, as subtitles cannot be
guaranteed to start at index 1
That being said, I can already think of a way to do this using an array and offset attribute

Public Class Methods

new(file, linebreak = "\r\n") click to toggle source

@param [String] file A string of the file name @param [String] linebreak A string of the linebreak pattern.

# File lib/subtitle_shifter.rb, line 56
def initialize(file, linebreak = "\r\n")
  @sub_file  = file
  @linebreak = linebreak
  @parsed_ok = false
end

Public Instance Methods

parse() click to toggle source

Parses the subtitles @note Always call only after initialising. @note If your subtitle file is UTF-8 encoded, and has a Byte Order Mark as its first few bytes,

the BOM will not be preserved when outputing the parsed and shifted subtitles. You probably don't
need it anyway

@example

sub.parse if sub.parsed_ok

@see en.wikipedia.org/wiki/UTF-8#Byte_order_mark Wikipedia's article on UTF-8 and BOM

# File lib/subtitle_shifter.rb, line 70
def parse
  raw_text  = File.open(@sub_file, 'r').read.force_encoding('UTF-8')
  raw_text.gsub!("\xEF\xBB\xBF".force_encoding("UTF-8"), '') #Remove stupid BOM that was causing me so much grief!

  #raw_text       = IO.read @sub_file
  subtitle_parts = raw_text.split "#{@linebreak}#{@linebreak}"
  @subtitles     = {}

  subtitle_parts.each do |subtitle|
    @subtitles.update extract_sub_data subtitle
  end

  # No longer needed due to removal of BOM
  #fix_first_index   # What a hack :(
  @parsed_ok = true # Not very useful, but will help when error checking is added
end
shift(args) click to toggle source

Shifts subtitles forward (or backward) by a number of ms from an index @param [Integer] :index The index of the subtitle @param [Integer] :time The time (in ms) by which you wish to shift the subtitles. A negative value will shift backwards. @example

sub.shift(:index => 42, :time => 10000) # Shift subs from index 42 onwards by 10 seconds.

@raise [RuntimeError] Raises this exception when shifting backwards if index and index-1 time's overlap

# File lib/subtitle_shifter.rb, line 93
def shift(args)
  first = args[:index] # used for checking first go round.
  index = first
  shift = args[:time]

  if shift < 0 # backward shift check
    time1 = @subtitles[first][:start] + shift
    time2 = @subtitles[first-1][:end]
    raise RuntimeError, 'Cannot overlap backward shift' if time2 > time1
  end

  loop do
    break unless @subtitles.has_key?(index)

    @subtitles[index][:start] += shift
    @subtitles[index][:end]   += shift

    index += 1
  end
end
to_s() click to toggle source

Outputs parsed subtitles @raise [RuntimeError] Will raise this exception if an attempt is made to output the subs before parsing has taken place @see SubtitleShifter#parsed_ok

# File lib/subtitle_shifter.rb, line 117
def to_s
  raise RuntimeError, 'File has not been parsed yet' unless @parsed_ok

  output = ''

  @subtitles.sort.map do |index, sub| 
    start = ms_to_srt_time sub[:start]
    fin   = ms_to_srt_time sub[:end]

    output += "#{index}#{@linebreak}#{start} #{TIME_SEPERATOR} #{fin}#{@linebreak}#{sub[:subtitle]}#{@linebreak}#{@linebreak}"
  end

  output.chomp
end

Private Instance Methods

extract_sub_data(subtitle) click to toggle source
# File lib/subtitle_shifter.rb, line 134
def extract_sub_data(subtitle)
  s     = subtitle.split @linebreak
  times = s[1].split " #{TIME_SEPERATOR} "

  {s[0].to_i => {
    start:    srt_time_to_ms(times[0]),
    end:      srt_time_to_ms(times[1]),
    subtitle: s[2..-1].join(@linebreak)
    }
  }
end
fix_first_index() click to toggle source

No longer needed due to fixing the BOM issue. But I'm leaving it in.

# File lib/subtitle_shifter.rb, line 178
def fix_first_index
  # This makes me feel *so* dirty :/
  sub_arr = @subtitles.to_a
  idx1    = sub_arr[0][0]
  idx2    = sub_arr[1][0]

  @subtitles[idx2 - 1] = @subtitles.delete idx1 # At least I learnt this trick :) How to rename a hash key
end
ms_to_srt_time(ms) click to toggle source
# File lib/subtitle_shifter.rb, line 156
def ms_to_srt_time(ms)
  hours   = (ms / (1000 * 60 *60)) % 60
  minutes = (ms / (1000 * 60))     % 60
  seconds = (ms / 1000)            % 60
  adj_ms  = ms.to_s[-3..-1].to_i

  "%02d:%02d:%02d,%03d" % [hours, minutes, seconds, adj_ms]
end
parse_srt_time(srt_time) click to toggle source
# File lib/subtitle_shifter.rb, line 165
def parse_srt_time (srt_time)
  # Time looks like: hh:mm:ss,ms
  #              ... 10:09:08,756
  /^(\d+):(\d+):(\d+),(\d+)$/ =~ srt_time

  {hours: $1.to_i,
   mins:  $2.to_i,
   secs:  $3.to_i,
   ms:    $4.to_i
  }
end
srt_time_to_ms(srt_time) click to toggle source
# File lib/subtitle_shifter.rb, line 146
def srt_time_to_ms(srt_time)
  time_parts = parse_srt_time srt_time

  hours_ms = time_parts[:hours] * 60 * 60 * 1000
  mins_ms  = time_parts[:mins]  * 60 * 1000
  secs_ms  = time_parts[:secs]  * 1000

  hours_ms + mins_ms + secs_ms + time_parts[:ms]
end