encoder-tools 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +83 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +17 -0
  7. data/Rakefile +46 -0
  8. data/VERSION +1 -0
  9. data/bin/encoder-tools +4 -0
  10. data/lib/encoder-tools/cli/base.rb +40 -0
  11. data/lib/encoder-tools/cli/subtitles/base.rb +15 -0
  12. data/lib/encoder-tools/cli/subtitles/fix_lengths.rb +46 -0
  13. data/lib/encoder-tools/cli/subtitles/offset.rb +30 -0
  14. data/lib/encoder-tools/cli/subtitles/renumber.rb +11 -0
  15. data/lib/encoder-tools/cli/subtitles.rb +10 -0
  16. data/lib/encoder-tools/cli.rb +30 -0
  17. data/lib/encoder-tools/options/title.rb +34 -0
  18. data/lib/encoder-tools/options.rb +5 -0
  19. data/lib/encoder-tools/strategies/base.rb +22 -0
  20. data/lib/encoder-tools/strategies/movie.rb +10 -0
  21. data/lib/encoder-tools/strategies/tv.rb +10 -0
  22. data/lib/encoder-tools/strategies.rb +7 -0
  23. data/lib/encoder-tools/subtitles/list.rb +35 -0
  24. data/lib/encoder-tools/subtitles/parser.rb +96 -0
  25. data/lib/encoder-tools/subtitles/relaxed_parser.rb +11 -0
  26. data/lib/encoder-tools/subtitles/subtitle.rb +47 -0
  27. data/lib/encoder-tools/subtitles.rb +8 -0
  28. data/lib/encoder-tools.rb +7 -0
  29. data/spec/cli/subtitles/fix_lengths_spec.rb +51 -0
  30. data/spec/cli/subtitles/offset_spec.rb +40 -0
  31. data/spec/cli/subtitles/renumber_spec.rb +23 -0
  32. data/spec/fixtures/subtitles/adjusted-long-subtitles.srt +16 -0
  33. data/spec/fixtures/subtitles/bad-numbering-corrected.srt +16 -0
  34. data/spec/fixtures/subtitles/bad-numbering.srt +16 -0
  35. data/spec/fixtures/subtitles/kill-bill-vol-2.srt +345 -0
  36. data/spec/fixtures/subtitles/no-long-subtitles.srt +16 -0
  37. data/spec/fixtures/subtitles/short-example-offset-2.srt +16 -0
  38. data/spec/fixtures/subtitles/short-example-offset-minus-2.srt +16 -0
  39. data/spec/fixtures/subtitles/short-example-offset-plus-2.srt +16 -0
  40. data/spec/fixtures/subtitles/short-example.srt +16 -0
  41. data/spec/fixtures/subtitles/some-long-subtitles.srt +16 -0
  42. data/spec/options/title_spec.rb +41 -0
  43. data/spec/spec.opts +2 -0
  44. data/spec/spec_helper.rb +31 -0
  45. data/spec/strategies/movie_spec.rb +23 -0
  46. data/spec/subtitles/list_spec.rb +57 -0
  47. data/spec/subtitles/subtitle_spec.rb +31 -0
  48. metadata +138 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ .bundle
23
+ encoder-tools.gemspec
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://gemcutter.org"
2
+
3
+ gem "thor"
4
+
5
+ group :test do
6
+ gem "jeweler"
7
+ gem "rake"
8
+ gem "yard"
9
+ gem "rspec", "~>1.3.0"
10
+ gem "bundler", "~>0.9.4"
11
+ gem "ruby-debug"
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,83 @@
1
+ ---
2
+ dependencies:
3
+ ruby-debug:
4
+ group:
5
+ - :test
6
+ version: ">= 0"
7
+ require:
8
+ - ruby-debug
9
+ rake:
10
+ group:
11
+ - :test
12
+ version: ">= 0"
13
+ require:
14
+ - rake
15
+ thor:
16
+ group:
17
+ - :default
18
+ version: ">= 0"
19
+ require:
20
+ - thor
21
+ rspec:
22
+ group:
23
+ - :test
24
+ version: ~> 1.3.0
25
+ require:
26
+ - rspec
27
+ yard:
28
+ group:
29
+ - :test
30
+ version: ">= 0"
31
+ require:
32
+ - yard
33
+ bundler:
34
+ group:
35
+ - :test
36
+ version: ~> 0.9.4
37
+ require:
38
+ - bundler
39
+ jeweler:
40
+ group:
41
+ - :test
42
+ version: ">= 0"
43
+ require:
44
+ - jeweler
45
+ specs:
46
+ - json_pure:
47
+ version: 1.2.0
48
+ - bundler:
49
+ version: 0.9.4
50
+ - git:
51
+ version: 1.2.5
52
+ - linecache:
53
+ version: "0.43"
54
+ - ruby-debug-base:
55
+ version: 0.10.3
56
+ - json:
57
+ version: 1.1.9
58
+ - net-ssh:
59
+ version: 2.0.15
60
+ - net-scp:
61
+ version: 1.0.1
62
+ - gemcutter:
63
+ version: 0.1.3
64
+ - rspec:
65
+ version: 1.3.0
66
+ - yard:
67
+ version: 0.5.3
68
+ - rubyforge:
69
+ version: 2.0.3
70
+ - jeweler:
71
+ version: 1.4.0
72
+ - thor:
73
+ version: 0.13.1
74
+ - rake:
75
+ version: 0.8.7
76
+ - columnize:
77
+ version: 0.3.1
78
+ - ruby-debug:
79
+ version: 0.10.3
80
+ hash: 075129d1fa532f8d303254e1ef70185e252d1ce4
81
+ sources:
82
+ - Rubygems:
83
+ uri: http://gemcutter.org
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Brian Donovan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = encoder-tools
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Brian Donovan. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "encoder-tools"
8
+ gem.summary = %Q{Some tools to make encoding from DVDs easier}
9
+ gem.description = %Q{Tools for ripping, encoding, and subtitling movies and TV shows}
10
+ gem.email = "brian.donovan@gmail.com"
11
+ gem.homepage = "http://github.com/eventualbuddha/encoder-tools"
12
+ gem.authors = ["Brian Donovan"]
13
+ gem.add_dependency "thor", ">= 0"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ gem.add_development_dependency "yard", ">= 0"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ begin
40
+ require 'yard'
41
+ YARD::Rake::YardocTask.new
42
+ rescue LoadError
43
+ task :yardoc do
44
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
45
+ end
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
data/bin/encoder-tools ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'encoder-tools'
4
+ EncoderTools::CLI.start
@@ -0,0 +1,40 @@
1
+ module EncoderTools
2
+ class CLI
3
+ class Base
4
+ attr_reader :shell, :options
5
+
6
+ def initialize(shell, options={})
7
+ @shell, @options = shell, options
8
+ end
9
+
10
+ def run
11
+ # overridden in subclasses
12
+ end
13
+
14
+ private
15
+ def input
16
+ @input ||= @options[:input] ?
17
+ open(@options[:input]) :
18
+ $stdin
19
+ end
20
+
21
+ def output
22
+ @output ||= @options[:output] ?
23
+ open(@options[:output], 'w') :
24
+ $stdout
25
+ end
26
+
27
+ def open(stream_or_file, mode='r')
28
+ if stream_or_file.respond_to?(:eof?)
29
+ stream_or_file
30
+ else
31
+ File.open(stream_or_file, mode)
32
+ end
33
+ end
34
+
35
+ def self.run(shell, options={})
36
+ new(shell, options).run
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ module EncoderTools
2
+ class CLI
3
+ module Subtitles
4
+ class Base < CLI::Base
5
+ def parse(text)
6
+ EncoderTools::Subtitles::List.load(text)
7
+ end
8
+
9
+ def parse_relaxed(text)
10
+ EncoderTools::Subtitles::List.load(text, EncoderTools::Subtitles::RelaxedParser)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ module EncoderTools
2
+ class CLI
3
+ module Subtitles
4
+ class FixLengths < Base
5
+ THRESHOLD = 5
6
+
7
+ def run
8
+ if long_subtitles.empty?
9
+ shell.say "No subtitles found over #{THRESHOLD}s"
10
+ return
11
+ end
12
+
13
+ if not shell.yes?("Found #{long_subtitles.size} long subtitles. Would you like to fix them?")
14
+ return
15
+ end
16
+
17
+ long_subtitles.each do |subtitle|
18
+ lines = subtitle.to_s.to_a
19
+ range = lines.shift.chomp
20
+ range += " (#{subtitle.duration.to_i}s)\n"
21
+ lines.unshift(range)
22
+ lines << "\n" << "\n"
23
+
24
+ shell.say(lines.join)
25
+ subtitle.duration = shell.ask("How long should it be?").to_i
26
+ end
27
+
28
+ output << list.to_s
29
+
30
+ return nil
31
+ end
32
+
33
+ private
34
+ def long_subtitles
35
+ @long_subtitles ||= list.entries.select do |subtitle|
36
+ subtitle.duration > THRESHOLD
37
+ end
38
+ end
39
+
40
+ def list
41
+ @list ||= parse(input.read)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ module EncoderTools
2
+ class CLI
3
+ module Subtitles
4
+ class Offset < Base
5
+ def run
6
+ output << offset(parse(input.read), options[:offset]).to_s
7
+ end
8
+
9
+ protected
10
+ def offset(list, offset)
11
+ list.offset = parse_offset(list, offset)
12
+ return list
13
+ end
14
+
15
+ def parse_offset(list, offset)
16
+ case offset
17
+ when Fixnum
18
+ offset
19
+ when /^\+(\d+)$/
20
+ list.offset + $1.to_i
21
+ when /^-(\d+)$/
22
+ list.offset - $1.to_i
23
+ when /^\d+$/
24
+ offset.to_i
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ module EncoderTools
2
+ class CLI
3
+ module Subtitles
4
+ class Renumber < Base
5
+ def run
6
+ output << parse_relaxed(input.read).to_s
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module EncoderTools
2
+ class CLI
3
+ module Subtitles
4
+ autoload :Base, 'encoder-tools/cli/subtitles/base'
5
+ autoload :FixLengths, 'encoder-tools/cli/subtitles/fix_lengths'
6
+ autoload :Offset, 'encoder-tools/cli/subtitles/offset'
7
+ autoload :Renumber, 'encoder-tools/cli/subtitles/renumber'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,30 @@
1
+ require 'thor'
2
+ require 'thor/group'
3
+
4
+ module EncoderTools
5
+ class CLI < Thor
6
+ autoload :Base, 'encoder-tools/cli/base'
7
+ autoload :Subtitles, 'encoder-tools/cli/subtitles'
8
+
9
+ desc "renumber [--input FILE] [--output FILE]", "Renumber badly-numbered SRT subtitle text"
10
+ method_option :input, :type => :string, :required => false, :aliases => %w[-i]
11
+ method_option :output, :type => :string, :required => false, :aliases => %w[-o]
12
+ def renumber
13
+ CLI::Subtitles::Renumber.run(self, options)
14
+ end
15
+
16
+ desc "offset [--input FILE] [--output FILE] [+-]OFFSET", "Change the SRT subtitle offset to OFFSET or by +/-OFFSET"
17
+ method_option :input, :type => :string, :required => false, :aliases => %w[-i]
18
+ method_option :output, :type => :string, :required => false, :aliases => %w[-o]
19
+ def offset(offset)
20
+ CLI::Subtitles::Offset.run(self, options.merge(:offset => offset))
21
+ end
22
+
23
+ desc "fix-lengths --input FILE --output FILE", "Interactively fix subtitle lengths over a #{CLI::Subtitles::FixLengths::THRESHOLD}s threshold"
24
+ method_option :input, :type => :string, :required => true, :aliases => %w[-i]
25
+ method_option :output, :type => :string, :required => true, :aliases => %w[-o]
26
+ def fix_lengths
27
+ CLI::Subtitles::FixLengths.run(self, options)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ require 'singleton'
2
+
3
+ module EncoderTools
4
+ module Options
5
+ class Title
6
+ attr_accessor :number
7
+
8
+ def initialize(number)
9
+ @number = number
10
+ end
11
+
12
+ def ==(other)
13
+ other.is_a?(self.class) && other.number == self.number
14
+ end
15
+
16
+ def to_args
17
+ ['--title', number.to_s]
18
+ end
19
+
20
+ class Longest < Title
21
+ include Singleton
22
+
23
+ def initialize
24
+ super(nil)
25
+ end
26
+
27
+ def to_args
28
+ %w[--longest]
29
+ end
30
+ end
31
+ LONGEST = Longest.instance
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ module EncoderTools
2
+ module Options
3
+ autoload :Title, 'encoder-tools/options/title'
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ module EncoderTools
2
+ module Strategies
3
+ class Base
4
+ attr_reader :input_path, :title
5
+
6
+ def initialize(input_path)
7
+ @input_path = input_path
8
+ end
9
+
10
+ def title=(title)
11
+ case title
12
+ when Options::Title, nil
13
+ @title = title
14
+ when Fixnum
15
+ @title = Options::Title.new(title)
16
+ else
17
+ raise ArgumentError, "expected an #{Options::Title} or #{Fixnum}, got #{title.inspect}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ module EncoderTools
2
+ module Strategies
3
+ class Movie < Base
4
+ def initialize(input_path)
5
+ super
6
+ self.title = Options::Title::LONGEST
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EncoderTools
2
+ module Strategies
3
+ class TV < Base
4
+ def initialize(input_path)
5
+ super
6
+ self.title = 1
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module EncoderTools
2
+ module Strategies
3
+ autoload :Base, 'encoder-tools/strategies/base'
4
+ autoload :Movie, 'encoder-tools/strategies/movie'
5
+ autoload :TV, 'encoder-tools/strategies/tv'
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ module EncoderTools
2
+ module Subtitles
3
+ class List
4
+ attr_accessor :entries
5
+
6
+ def offset
7
+ return 0 if entries.empty?
8
+ entries.first.offset
9
+ end
10
+
11
+ def offset=(offset)
12
+ return nil if entries.empty?
13
+ diff = offset - self.offset
14
+ entries.each {|subtitle| subtitle.offset += diff}
15
+ end
16
+
17
+ def to_s
18
+ str = ""
19
+ i = 0
20
+ entries.each do |subtitle|
21
+ str << (i += 1).to_s << "\n" << subtitle.to_s << "\n\n"
22
+ end
23
+
24
+ return str
25
+ end
26
+
27
+ def self.load(input, parser_class=Parser)
28
+ result = new
29
+ parser = parser_class.new(input)
30
+ result.entries = parser.parse
31
+ return result
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,96 @@
1
+ require 'strscan'
2
+ require 'bigdecimal'
3
+
4
+ module EncoderTools
5
+ module Subtitles
6
+ class Parser
7
+ class ParseError < RuntimeError; end
8
+
9
+ def initialize(input)
10
+ @scanner = StringScanner.new(input)
11
+ @last_index = 0
12
+ end
13
+
14
+ def parse
15
+ result = []
16
+ result << scan_subtitle until @scanner.eos?
17
+ return result
18
+ end
19
+
20
+ protected
21
+ def scan_subtitle
22
+ index = scan_index
23
+ range = scan_timestamp_range
24
+ text = scan_text
25
+ return Subtitle.new(range, text)
26
+ end
27
+
28
+ def scan_index
29
+ # 1\n
30
+ index = string @last_index += 1
31
+ newline
32
+ return index
33
+ end
34
+
35
+ def scan_timestamp_range
36
+ # 01:15:18,000 --> 01:15:20,300\n
37
+ rstart = timestamp
38
+ string ' --> '
39
+ rend = timestamp
40
+ newline
41
+ return rstart..rend
42
+ end
43
+
44
+ def scan_text
45
+ # No wonder you can't do it... you acquiesce\n
46
+ # to defeat... before you even begin.\n
47
+ # \n
48
+ text = ''
49
+ loop do
50
+ l = line
51
+ break if l.nil? || l.strip.empty?
52
+ text << l
53
+ end
54
+ text.strip!
55
+
56
+ return text
57
+ end
58
+
59
+ def string(str)
60
+ scan /#{Regexp.escape(str.to_s)}/
61
+ end
62
+
63
+ def timestamp
64
+ hours = scan /\d\d/, 'hours'
65
+ string ':'
66
+ minutes = scan /\d\d/, 'minutes'
67
+ string ':'
68
+ seconds = scan /\d\d/, 'seconds'
69
+ string ','
70
+ millis = scan /\d\d\d/, 'milliseconds'
71
+
72
+ return hours.to_i * 3600 + minutes.to_i * 60 + seconds.to_i + (BigDecimal(millis) / 1000)
73
+ end
74
+
75
+ def newline
76
+ scan /\r?\n/, 'newline'
77
+ end
78
+
79
+ def line
80
+ @scanner.scan_until(/\r?\n/)
81
+ end
82
+
83
+ def scan(scannable, name=nil)
84
+ if scanned = @scanner.scan(scannable)
85
+ return scanned
86
+ else
87
+ raise ParseError, "expected #{name || scannable.inspect} at character #{@scanner.pos}, got #{@scanner.string[@scanner.pos, 10].inspect}"
88
+ end
89
+ end
90
+
91
+ def upto(scannable)
92
+ @scanner.scan_until(scannable)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ module EncoderTools
2
+ module Subtitles
3
+ class RelaxedParser < Parser
4
+ protected
5
+ def scan_index
6
+ scan /\d+/
7
+ newline
8
+ end
9
+ end
10
+ end
11
+ end