storyboard 0.4.1 → 0.5.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- storyboard (0.4.0)
4
+ storyboard (0.5.0.pre2)
5
+ bundler
5
6
  levenshtein-ffi
6
7
  mime-types (>= 1.19)
7
8
  mini_magick
8
- nokogiri
9
9
  path (>= 1.3.0)
10
10
  prawn
11
11
  ruby-progressbar
@@ -17,7 +17,6 @@ PATH
17
17
  specs:
18
18
  suby (0.4.0)
19
19
  mime-types (>= 1.19)
20
- nokogiri
21
20
  path (>= 1.3.0)
22
21
  rubyzip
23
22
  term-ansicolor
@@ -35,7 +34,6 @@ GEM
35
34
  mime-types (1.19)
36
35
  mini_magick (3.4)
37
36
  subexec (~> 0.2.1)
38
- nokogiri (1.5.6)
39
37
  path (1.3.1)
40
38
  pdf-reader (1.3.0)
41
39
  Ascii85 (~> 1.0.0)
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013 Mark Olson <theothermarkolson@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Storyboard
2
2
 
3
- Read the TV and Movies you don't have time to watch. Given a video file, it will generate a PDF (or soon, ePub and Mobi) containing every scene change and line of dialog.
4
-
5
- ## Storyboard
6
-
7
3
  ![Seinfeld](http://i.imgur.com/lTRuC.jpg)
8
4
 
5
+ Read the TV and Movies you don't have time to watch. Given a video file, it will generate a PDF (or soon, ePub and Mobi) containing every scene change and line of dialog.
6
+ [I wrote a blog post](http://syntaxi.net/2013/01/20/storyboard) with a few more details about how Storyboard works.
7
+
9
8
  Storyboard is _very much_ a work in progress, though it works most of the time. Using it is simple:
10
9
 
11
10
  $ storyboard /path/to/video-file.mkv
@@ -24,7 +23,7 @@ To quickly test if the subtitles that are used look ok, you can use the `--previ
24
23
 
25
24
  If the subtitles are off, you can nudge them back or forward with the `-n TIME` option. This can be positive or negative, and if you make it too large it can cause Storyboard to throw an error. This would nudge the subtitles back 2 seconds, and generate just the preview PDF.
26
25
 
27
- storyboard -n -2 --preview /path/to/video-file.mkv
26
+ storyboard -n -2 --preview /path/to/video-file.mkv
28
27
 
29
28
  You can see all the available options by using the help option:
30
29
 
data/bin/gifboard ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'shellwords'
5
+ require 'optparse'
6
+ require 'logger'
7
+ require 'open3'
8
+ require 'bundler'
9
+ ENV['BUNDLE_GEMFILE'] = File.join(File.dirname(__FILE__), '../Gemfile')
10
+ Bundler.require(:default)
11
+
12
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
13
+ require 'gifboard'
14
+
15
+ unless Storyboard.ffprobe_installed?
16
+ puts "Storyboard requires FFmpeg 1.1. Please check the README for instructions"
17
+ exit
18
+ end
19
+
20
+ unless Storyboard.magick_installed?
21
+ puts "Storyboard requires Imagemagick's mogrify tool. Please check the README for instructions on how to install Imagemagick if you need them."
22
+ exit
23
+ end
24
+
25
+ options = {:nudge => 0, :verbose => true, :max_width => 500}
26
+
27
+ puts "Running Gifboard #{Storyboard::VERSION}"
28
+
29
+ LOG = Logger.new(STDOUT)
30
+ LOG.level = Logger::INFO
31
+
32
+ opts = OptionParser.new
33
+ opts.banner = "Usage: gifboard [options] videofile [output_directory]"
34
+
35
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
36
+ options[:version] = v
37
+ LOG.level = Logger::DEBUG if v
38
+ end
39
+
40
+ opts.on('-t "[text]"', "Subtitle text to build the GIF around") do |t|
41
+ options[:text] = t
42
+ end
43
+ opts.on("-n", "--nudge TIME", Float, "Nudge the subtitles forward or backward. TIME is the number of seconds.", "Use this with the --preview option to quickly check and adjust the subtitle timings.") do |time|
44
+ options[:nudge] = time
45
+ end
46
+
47
+
48
+ opts.on("-s", "--subs FILE", "SRT subtitle file to use. Will skip extracting/downloading one.") do |s|
49
+ options[:subs] = s
50
+ end
51
+
52
+
53
+ opts.on_tail("-h", "--help", "Show this message") do
54
+ puts opts
55
+ exit
56
+ end
57
+
58
+ begin opts.parse! ARGV
59
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
60
+ puts e
61
+ puts opts
62
+ exit 1
63
+ end
64
+
65
+ if ARGV.size < 1
66
+ puts "videofile required"
67
+ puts opts.to_s
68
+ exit 1
69
+ elsif ARGV.size == 2 && !File.directory?(File.realdirpath(ARGV.last))
70
+ puts "outputdir #{ARGV.last} is not a directory"
71
+ puts opts.to_s
72
+ exit 1
73
+ elsif ARGV.size > 2
74
+ puts "Too many arguments"
75
+ puts opts.to_s
76
+ exit 1
77
+ end
78
+
79
+ options[:vidpath] = File.realdirpath(ARGV.shift)
80
+ options[:write_to] = ARGV.shift || Dir.pwd
81
+
82
+ if !File.exists?(options[:vidpath])
83
+ puts("#{options[:vidpath]} doesn't exist.")
84
+ exit
85
+ end
86
+
87
+ filepaths = []
88
+
89
+ if File.directory?(options[:vidpath])
90
+ raise "Gifboard can only use one file at a time. Don't pass a directory."
91
+ exit
92
+ else
93
+ filepaths << options[:vidpath]
94
+ end
95
+
96
+ filepaths.each {|fp|
97
+ options[:file] = fp
98
+ runner = Gifboard.new(options)
99
+ runner.run if runner.video_file?
100
+ }
Binary file
Binary file
data/lib/gifboard.rb ADDED
@@ -0,0 +1,106 @@
1
+ require 'storyboard/subtitles.rb'
2
+ require 'storyboard/bincheck.rb'
3
+ require 'storyboard/thread-util.rb'
4
+ require 'storyboard/time.rb'
5
+ require 'storyboard/version.rb'
6
+ require 'storyboard/cache.rb'
7
+
8
+ require 'storyboard/generators/sub.rb'
9
+ require 'storyboard/generators/gif.rb'
10
+
11
+ require 'mime/types'
12
+ require 'fileutils'
13
+ require 'tmpdir'
14
+
15
+ require 'ruby-progressbar'
16
+ require 'mini_magick'
17
+
18
+ require 'json'
19
+
20
+ class Gifboard < Storyboard
21
+ attr_accessor :options, :capture_points, :subtitles, :timings
22
+ attr_accessor :length, :renderers, :mime, :cache
23
+
24
+ def initialize(o)
25
+ super
26
+ end
27
+
28
+ def run
29
+ LOG.info("Processing #{options[:file]}")
30
+ setup
31
+
32
+ @cache = Cache.new(Suby::MovieHasher.compute_hash(Path.new(options[:file])))
33
+
34
+ LOG.debug(options) if options[:verbose]
35
+
36
+ @subtitles = SRT.new(options[:subs] ? File.read(options[:subs]) : get_subtitles, options)
37
+ # bit of a temp hack so I don't have to wait all the time.
38
+ @subtitles.save if options[:verbose]
39
+
40
+ @cache.save
41
+
42
+ if @options[:text]
43
+ @renderers << Storyboard::GifRenderer.new(self)
44
+
45
+ selected = choose_text
46
+ @capture_points << selected.start_time
47
+ (0.1).step(selected.end_time.value - selected.start_time.value, 0.1) {|i|
48
+ @capture_points << selected.start_time + i
49
+ }
50
+ # @capture_points << selected.end_time not sure if it's better or worse to leave this yet
51
+
52
+ @stop_frame = @capture_points.count
53
+ extract_frames
54
+
55
+ render_output
56
+ else
57
+ @subtitles.pages.each {|x|
58
+ print "[#{x.start_time.to_srt}]\t"
59
+ x.lines.each_with_index{|l,i|
60
+ print "\t\t" if i > 0
61
+ puts l
62
+ }
63
+ }
64
+ puts "\n\nYou need to specify what text to look for with the -t option. Listing all subtitles instead."
65
+ puts "ex: gifboard -t 'a funny joke ha. ha' video.mkv"
66
+ end
67
+ end
68
+
69
+ def choose_text
70
+ matches = []
71
+ @subtitles.pages.each {|x|
72
+ found = !x.lines.select {|l|
73
+ l.downcase.match(options[:text].downcase)
74
+ }.empty?
75
+ matches << x if found
76
+ }
77
+
78
+ match = nil
79
+ if matches.count == 0
80
+ raise "No matches found"
81
+ elsif matches.count == 1
82
+ puts "Just one match found. Using it."
83
+ match = matches.first
84
+ else
85
+ puts "Multiple matches found.. pick one!"
86
+ matches.each_with_index {|m,i|
87
+ print "#{i+1}:\t"
88
+ m.lines.each_with_index{|l,j|
89
+ print "\t" if j > 0
90
+ puts l
91
+ }
92
+ }
93
+ while !match
94
+ print "choice (default 1): "
95
+ input = gets.chomp
96
+ number = input.empty? ? 1 : input.to_i
97
+ if number > matches.count || number < 1
98
+ puts "Try again. Choose a subtitle between 1 and #{matches.count}"
99
+ else
100
+ match = matches[number-1]
101
+ end
102
+ end
103
+ end
104
+ match
105
+ end
106
+ end
data/lib/storyboard.rb CHANGED
@@ -3,6 +3,8 @@ require 'storyboard/bincheck.rb'
3
3
  require 'storyboard/thread-util.rb'
4
4
  require 'storyboard/time.rb'
5
5
  require 'storyboard/version.rb'
6
+ require 'storyboard/cache.rb'
7
+ require 'storyboard/common.rb'
6
8
 
7
9
  require 'storyboard/generators/sub.rb'
8
10
  require 'storyboard/generators/pdf.rb'
@@ -14,26 +16,60 @@ require 'tmpdir'
14
16
  require 'ruby-progressbar'
15
17
  require 'mini_magick'
16
18
 
19
+ require 'json'
20
+
17
21
  class Storyboard
18
22
  attr_accessor :options, :capture_points, :subtitles, :timings
19
- attr_accessor :length, :renderers, :mime
23
+ attr_accessor :length, :renderers, :mime, :cache, :encoding
24
+ attr_accessor :needs_KFhimaji
20
25
 
21
26
  def initialize(o)
27
+ @needs_KFhimaji = false
22
28
  @capture_points = []
23
29
  @renderers = []
24
30
  @options = o
25
-
31
+ @encoding = "UTF-8"
26
32
  check_video
27
33
  end
28
34
 
35
+ def self.needs_KFhimaji(set = false)
36
+ @needs_KFhimaji ||= set
37
+ end
38
+
39
+ def self.path
40
+ File.dirname(__FILE__) + '/../'
41
+ end
42
+
43
+ def self.current_encoding
44
+ @encoding || 'UTF-8'
45
+ end
46
+
47
+ def self.current_encoding=(n)
48
+ @encoding = n
49
+ end
50
+
51
+ def self.encode_regexp(r)
52
+ Regexp.new(r.encode(Storyboard.current_encoding), 16)
53
+ end
54
+
55
+ def self.encode_string(r)
56
+ r.encode(Storyboard.current_encoding)
57
+ end
58
+
29
59
  def run
30
60
  LOG.info("Processing #{options[:file]}")
31
61
  setup
32
62
 
63
+ @cache = Cache.new(Suby::MovieHasher.compute_hash(Path.new(options[:file])))
64
+
65
+ LOG.debug(options) if options[:verbose]
66
+
33
67
  @subtitles = SRT.new(options[:subs] ? File.read(options[:subs]) : get_subtitles, options)
34
68
  # bit of a temp hack so I don't have to wait all the time.
35
69
  @subtitles.save if options[:verbose]
36
70
 
71
+ @cache.save
72
+
37
73
  @renderers << Storyboard::PDFRenderer.new(self) if options[:types].include?('pdf')
38
74
 
39
75
  run_scene_detection if options[:scenes]
@@ -51,7 +87,7 @@ class Storyboard
51
87
  extract_frames
52
88
 
53
89
  render_output
54
-
90
+ exit
55
91
  cleanup
56
92
  end
57
93
 
@@ -0,0 +1,56 @@
1
+ require 'digest/sha1'
2
+
3
+ class Storyboard
4
+ class Cache
5
+ attr_accessor :file, :hash
6
+ def initialize(file_hash)
7
+ @hash = file_hash
8
+ @file = File.join(Dir.tmpdir, "#{file_hash}.storyboard")
9
+ if File.exists?(@file)
10
+ @data = JSON.parse(File.read(@file))
11
+ end
12
+ @data = {'downloads' => {}} if @data.nil? || old?
13
+ @data['lastran'] = Time.now.to_s
14
+ end
15
+
16
+ def old?
17
+ DateTime.parse(@data['lastran']) < (DateTime.now - ((60 * 15)/86400.0))
18
+ end
19
+
20
+ def save
21
+ File.open(@file, 'w') { |f| f.write(@data.to_json)}
22
+ end
23
+
24
+
25
+ def download_file(url, &block)
26
+ if @data['downloads'][url]
27
+ LOG.info("Cached file #{@data['downloads'][url]}")
28
+ return File.read(@data['downloads'][url])
29
+ else
30
+ LOG.info("Loading file from #{url}")
31
+ results = yield
32
+ subpath = File.join(Dir.tmpdir, "#{@hash}-#{Digest::SHA1.hexdigest(url)}.storyboard")
33
+ File.open(subpath, 'w') { |f| f.write(results) }
34
+ @data['downloads'][url] = subpath
35
+ self.save
36
+ return results
37
+ end
38
+ end
39
+
40
+ def subtitles
41
+ @data['subtitles']
42
+ end
43
+
44
+ def subtitles=(val)
45
+ @data['subtitles'] = val
46
+ end
47
+
48
+ def last_used_subtitle
49
+ @data['last_used_subtitle']
50
+ end
51
+
52
+ def last_used_subtitle=(val)
53
+ @data['last_used_subtitle'] = val
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ #encoding: utf-8
2
+ class String
3
+ def contains_cjk?
4
+ !!(self =~ Storyboard.encode_regexp('\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}'))
5
+ end
6
+ end
@@ -0,0 +1,41 @@
1
+ require 'prawn'
2
+ require 'mini_magick'
3
+
4
+ class Storyboard
5
+ class GifRenderer < Storyboard::Renderer
6
+
7
+ attr_accessor :pdf, :storyboard, :dimensions
8
+ def initialize(parent)
9
+ @dimensions = []
10
+ @storyboard = parent
11
+ end
12
+
13
+ def set_dimensions(w,h)
14
+ @dimensions = [w,h]
15
+ end
16
+
17
+ def write
18
+ `cd #{@storyboard.options[:save_directory]} && convert -coalesce -delay 10 -layers OptimizeTransparency -loop 0 -ordered-dither o8x8,8,8,4 +map sub-* "#{@storyboard.options[:write_to]}/#{@storyboard.options[:basename]}.gif"`
19
+ LOG.info("Wrote #{@storyboard.options[:write_to]}/#{@storyboard.options[:basename]}.gif")
20
+ end
21
+
22
+ def render_frame(frame_name, subtitle = nil)
23
+ output_filename = File.join(@storyboard.options[:save_directory], "sub-#{File.basename(frame_name)}")
24
+ image = MiniMagick::Image.open(frame_name)
25
+
26
+ if(@dimensions.empty?)
27
+ resize_height = (image[:height] * (@storyboard.options[:max_width].to_f/image[:width])).ceil
28
+ set_dimensions(storyboard.options[:max_width], resize_height)
29
+ end
30
+
31
+ image.resize "#{@dimensions[0]}x#{@dimensions[1]}"
32
+ image.quality("60")
33
+
34
+ self.add_subtitle(image, subtitle, @dimensions) if subtitle
35
+ image.format 'jpeg'
36
+ image.write(output_filename)
37
+ image.destroy!
38
+ end
39
+
40
+ end
41
+ end
@@ -30,9 +30,9 @@ class Storyboard
30
30
  end
31
31
 
32
32
  image.resize "#{@dimensions[0]}x#{@dimensions[1]}"
33
+ image.quality("60")
33
34
 
34
35
  self.add_subtitle(image, subtitle, @dimensions) if subtitle
35
-
36
36
  image.format 'jpeg'
37
37
  image.write(output_filename)
38
38
  image.destroy!
@@ -2,40 +2,54 @@ require 'prawn'
2
2
  class Storyboard
3
3
  class Renderer
4
4
  @@size_canvas = Prawn::Document.new
5
+
6
+ def write_mvg(offset, line, nudge=0)
7
+ out = File.open(File.join(@storyboard.options[:save_directory], 'tmp.mvg'), 'wt', encoding: 'UTF-8')
8
+ out.print("text #{(0+nudge).to_s}, #{(offset+nudge).to_s} '")
9
+ out.print line
10
+ out.print "'"
11
+ out.close
12
+ end
13
+
5
14
  def add_subtitle(image, subtitle, dimensions)
6
- offset = 0
7
- subtitle.lines.reverse.each_with_index {|caption,i|
8
- escaped = caption.gsub(/\\|'|"/) { |c| "\\#{c}" }
9
- font_size = 30
10
- text_width = dimensions[0] + 1
11
- while(text_width > (dimensions[0] * 0.9))
12
- font_size -= 1
13
- text_width = @@size_canvas.width_of(caption, :size => font_size)
14
- end
15
+ offset = 0
16
+ subtitle.lines.reverse.each_with_index {|caption,i|
17
+ escaped = caption.gsub('\'') {|s| "\\#{s}" }
18
+ font_size = 30
19
+ text_width = dimensions[0] + 1
20
+ while(text_width > (dimensions[0] * 0.9))
21
+ font_size -= 1
22
+ text_width = @@size_canvas.width_of(caption.encode!("utf-8"), :size => font_size)
23
+ end
24
+
25
+ font = Storyboard.needs_KFhimaji ? "#{Storyboard.path}/fonts/KFhimaji.otf" : "#{Storyboard.path}/fonts/DejaVuSans.ttf"
26
+
27
+ write_mvg(offset,escaped, 0)
28
+ image.combine_options do |c|
29
+ c.font font
30
+ c.fill "#333333"
31
+ c.strokewidth '1'
32
+ c.stroke '#000000'
33
+ c.pointsize font_size.to_s
34
+ c.gravity "south"
35
+ c.draw "@#{File.join(@storyboard.options[:save_directory],'tmp.mvg')}"
36
+ end
15
37
 
16
- image.combine_options do |c|
17
- c.font "helvetica"
18
- c.fill "#333333"
19
- c.strokewidth '1'
20
- c.stroke '#000000'
21
- c.pointsize font_size.to_s
22
- c.gravity "south"
23
- c.draw "text 0, #{offset} '#{escaped}'"
24
- end
25
38
 
26
- #and the shadow
27
- image.combine_options do |c|
28
- c.font "helvetica"
29
- c.fill "#ffffff"
30
- c.strokewidth '1'
31
- c.stroke 'transparent'
32
- c.pointsize font_size.to_s
33
- c.gravity "south"
34
- c.draw "text -2, #{offset-2} '#{escaped}'"
35
- end
39
+ write_mvg(offset,escaped, -2)
40
+ #and the shadow
41
+ image.combine_options do |c|
42
+ c.font font
43
+ c.fill "#ffffff"
44
+ c.strokewidth '1'
45
+ c.stroke 'transparent'
46
+ c.pointsize font_size.to_s
47
+ c.gravity "south"
48
+ c.draw "@#{File.join(@storyboard.options[:save_directory],'tmp.mvg')}"
49
+ end
36
50
 
37
- offset += (@@size_canvas.height_of(caption, :size => font_size)).ceil
38
- }
51
+ offset += (@@size_canvas.height_of(caption.encode!("utf-8"), :size => font_size)).ceil
52
+ }
39
53
  end
40
54
  end
41
55
  end
@@ -1,3 +1,5 @@
1
+ require 'pp'
2
+
1
3
  class Storyboard
2
4
  def get_subtitles
3
5
  extensionless = File.join(File.dirname(options[:file]), File.basename(options[:file], ".*") + '.srt')
@@ -24,78 +26,132 @@ class Storyboard
24
26
  return File.read(extensionless)
25
27
  end
26
28
 
27
- if false == "the file has subtitles embedded"
28
-
29
- else
30
- LOG.debug("No subtitles embeded. Using suby.")
31
- # suby includes a giant util library the guy also wrote
32
- # that it uses to call file.basename instead of File.basename(file),
33
- #but "file" has to be a "Path", so, whatever.
34
- suby_file = Path(options[:file])
35
- #downloader = Suby::Downloader::OpenSubtitles.new(suby_file, 'en')
36
- # try Addic7ed first, as, on average, it seems a bit better.
37
- downloader = nil
38
-
29
+ # suby includes a giant util library the guy also wrote
30
+ # that it uses to call file.basename instead of File.basename(file),
31
+ #but "file" has to be a "Path", so, whatever.
32
+ suby_file = Path(options[:file])
33
+ downloader = Suby::Downloader::OpenSubtitles.new(suby_file, 'en')
34
+ chosen = nil
35
+
36
+ if @cache.subtitles.nil?
37
+ LOG.info("No subtitles cache found")
38
+ @cache.subtitles = downloader.possible_urls
39
+ @cache.save
40
+ end
41
+ chosen = pick_best_subtitle(@cache.subtitles)
42
+ contents = @cache.download_file(chosen) do
43
+ downloader.extract(chosen)
44
+ end
45
+ contents
46
+ end
39
47
 
40
- if downloader.nil?
41
- LOG.info("Searching for subtitles on OpenSubtitles")
42
- downloader = Suby::Downloader::OpenSubtitles.new(suby_file, 'en')
48
+ private
49
+
50
+ def pick_best_subtitle(given)
51
+ given = sort_matches(given)
52
+ if given.length == 0
53
+ raise "No subtitles found."
54
+ elsif given.length == 1
55
+ return given[0]['SubDownloadLink']
56
+ elsif given.length > 1
57
+ sub = nil
58
+ puts "There are multiple subtitles that could work with this file. Please choose one!"
59
+ puts "All of these are subtitles made for this exact video file, so any should work." if given[0]['MatchedBy'] == 'moviehash'
60
+ while not sub
61
+ given.each_with_index {|s, i|
62
+ puts "#{i+1}: '#{s['SubFileName']}', added #{s['SubAddDate']}"
63
+ }
64
+ print "choice (default 1): "
65
+ input = gets.chomp
66
+ number = input.empty? ? 1 : input.to_i
67
+ if number > given.count || number < 1
68
+ puts "Try again. Choose a subtitle between 1 and #{given.count}"
69
+ else
70
+ sub = given[number-1]['SubDownloadLink']
71
+ end
43
72
  end
44
-
45
- LOG.debug("Found #{downloader.download_url}")
46
- #LOG.debug(downloader.found)
47
- downloader.extract(downloader.download_url)
73
+ return sub
48
74
  end
49
75
  end
50
76
 
77
+ def sort_matches(x)
78
+ # filter to only {"MatchedBy"=>"moviehash"}, if possible
79
+ # select only matching filesizes, if nonzero and matching
80
+ x
81
+ end
82
+
83
+ public
84
+
51
85
 
52
86
  class SRT
53
87
  Page = Struct.new(:index, :start_time, :end_time, :lines)
54
88
 
55
- TIME_REGEX = /\d{2}:\d{2}:\d{2}[,\.]\d{1,4}/
56
- attr_accessor :text, :pages, :options
89
+ SPAN_REGEX = '[[:digit:]]+:[[:digit:]]+:[[:digit:]]+[,\.][[:digit:]]+'
90
+ attr_accessor :text, :pages, :options, :encoding
57
91
 
58
92
  def initialize(contents, parent_options)
59
93
  @options = parent_options
60
94
  @text = contents
61
95
  @pages = []
96
+ @needs_KFhimaji = false
97
+ check_bom(@text.lines.first)
98
+ Storyboard.current_encoding = @encoding
99
+ @text = @text.force_encoding(Storyboard.current_encoding)
62
100
  parse
63
101
  clean_promos
64
102
  LOG.info("Parsed subtitle file. #{count} entries found.")
65
103
  end
66
104
 
105
+
106
+ def check_bom(line)
107
+ bom_check = line.force_encoding("UTF-8").lines.to_a[0].bytes.to_a
108
+ @encoding = 'UTF-8'
109
+ if bom_check[0..1] == [255,254]
110
+ @encoding = "UTF-16LE"
111
+ ret = line[2..6]
112
+ elsif bom_check[0..2] == [239,187,191]
113
+ @encoding = "UTF-8"
114
+ ret = line[3..6]
115
+ end
116
+ line
117
+ end
118
+
119
+
67
120
  #There are some horrid files, so I want to be able to have more than just a single regex
68
121
  #to parse the srt file. Eventually, handling these errors will be a thing to do.
69
122
  def parse
70
123
  phase = :line_no
71
124
  page = nil
125
+
72
126
  @text.each_line {|l|
73
127
  l = l.strip
74
- l = l.encode("UTF-32", :invalid=>:replace, :replace=>"?").encode("UTF-8")
75
- # Some files have BOM markers. Why? Why would you add a BOM marker.
76
- l.gsub!("\xEF\xBB\xBF", '') if page.nil?
128
+ #p l.bytes.to_a
77
129
  case phase
78
130
  when :line_no
79
- if l =~ /^\d+$/
131
+ l = l.gsub(Storyboard.encode_regexp('\W'),'')
132
+ if l =~ Storyboard.encode_regexp('^\d+$')
80
133
  page = Page.new(@pages.count + 1, nil, nil, [])
81
134
  phase = :time
82
135
  elsif !l.empty?
83
- raise "Bad SRT File: Should have a block number but got '#{l}' [#{l.bytes.to_a.join(',')}]"
136
+ raise "Bad SRT File: Should have a block number but got '#{l.force_encoding('UTF-8')}' [#{l.bytes.to_a.join(',')}]"
84
137
  end
85
138
  when :time
86
- if l =~ /^(#{TIME_REGEX}) --> (#{TIME_REGEX})$/
139
+
140
+ l = l.gsub(Storyboard.encode_regexp('[^\,\:[0-9] \-\>]'), '')
141
+ if l =~ Storyboard.encode_regexp("^(#{SPAN_REGEX}) --> (#{SPAN_REGEX})$")
87
142
  page[:start_time] = STRTime.parse($1) + @options[:nudge]
88
143
  page[:end_time] = STRTime.parse($2) + @options[:nudge]
89
144
  phase = :text
90
145
  else
91
- raise "Bad SRT File: Should have time range but got '#{l}'"
146
+ raise "Bad SRT File: Should have time range but got '#{l}'".force_encoding(Storyboard.current_encoding)
92
147
  end
93
148
  when :text
94
149
  if l.empty?
95
150
  phase = :line_no
96
151
  @pages << page
97
152
  else
98
- page[:lines] << l.gsub(/<\/?[^>]+?>/, '')
153
+ Storyboard.needs_KFhimaji(true) if l.contains_cjk?
154
+ page[:lines] << l.gsub(Storyboard.encode_regexp("<\/?[^>]*>"), "")
99
155
  end
100
156
  end
101
157
  }
@@ -104,10 +160,10 @@ class Storyboard
104
160
  # Strip out obnoxious "CREATED BY L33T DUD3" or "DOWNLOADED FROM ____" text
105
161
  def clean_promos
106
162
  @pages.delete_if {|page|
107
- !page[:lines].grep(/Subtitles downloaded/).empty? ||
108
- !page[:lines].grep(/addic7ed/).empty? ||
109
- !page[:lines].grep(/OpenSubtitles/).empty? ||
110
- !page[:lines].grep(/sync, corrected by/).empty? ||
163
+ !page[:lines].grep(Storyboard.encode_regexp('Subtitles downloaded')).empty? ||
164
+ !page[:lines].grep(Storyboard.encode_regexp('addic7ed')).empty? ||
165
+ !page[:lines].grep(Storyboard.encode_regexp('OpenSubtitles')).empty? ||
166
+ !page[:lines].grep(Storyboard.encode_regexp('sync, corrected by')).empty? ||
111
167
  false
112
168
  }
113
169
  end
@@ -1,11 +1,12 @@
1
1
  class STRTime
2
- REGEX = /(\d{1,2}):(\d{1,2}):(\d{2})[,\.](\d{1,3})/
3
-
2
+ REGEX = '([[:digit:]]+):([[:digit:]]+):([[:digit:]]+)[,\.]([[:digit:]]{3})'
4
3
  attr_reader :value
5
4
 
6
5
  class <<self
7
6
  def parse(str)
8
- hh,mm,ss,ms = str.scan(REGEX).flatten.map{|i| Float(i)}
7
+ hh,mm,ss,ms = str.scan(Storyboard.encode_regexp(REGEX)).flatten.map{|i|
8
+ Float(i.force_encoding("ASCII-8bit").delete("\000"))
9
+ }
9
10
  value = ((((hh*60)+mm)*60)+ss) + ms/1000
10
11
  self.new(value)
11
12
  end
@@ -1,3 +1,3 @@
1
1
  class Storyboard
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0.pre3"
3
3
  end
data/storyboard.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib","vendor/suby"]
17
17
  gem.version = Storyboard::VERSION
18
18
 
19
- gem.add_dependency 'nokogiri'
19
+ gem.add_dependency 'bundler'
20
20
  gem.add_dependency 'mini_magick'
21
21
  gem.add_dependency 'prawn'
22
22
  gem.add_dependency 'ruby-progressbar'
@@ -1,6 +1,5 @@
1
1
  require 'net/http'
2
2
  require 'cgi/util'
3
- require 'nokogiri'
4
3
  require 'xmlrpc/client'
5
4
  require 'zlib'
6
5
  require 'stringio'
@@ -172,6 +171,4 @@ end
172
171
  # Defines downloader order
173
172
  %w[
174
173
  opensubtitles
175
- tvsubtitles
176
- addic7ed
177
174
  ].each { |downloader| require_relative "downloader/#{downloader}" }
@@ -30,17 +30,18 @@ module Suby
30
30
  }
31
31
  LANG_MAPPING.default = 'all'
32
32
 
33
- def download_url
33
+ def possible_urls
34
34
  s = SEARCH_QUERIES_ORDER.find(lambda { raise NotFoundError, "no subtitles available" }) { |type|
35
35
  if subs = search_subtitles(search_query(type))['data']
36
36
  @type = type
37
37
  break subs
38
38
  end
39
39
  }
40
- x = @video_data[:season] ? s.select { |t| t['SeriesSeason'].to_i == @video_data[:season].to_i } : s
41
- x.sort_by! { |t| Levenshtein.distance(t['MovieName'], "\"#{@video_data[:show]}\" #{@video_data[:title]}").to_i }
42
- @found = x.first
43
- x.first['SubDownloadLink']
40
+ return s
41
+ end
42
+
43
+ def download_url(no_this_one=nil)
44
+ (no_this_one || possible_urls[0])['SubDownloadLink']
44
45
  end
45
46
 
46
47
  def search_subtitles(query)
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
 
12
12
  s.required_ruby_version = '>= 1.9.2'
13
13
  s.add_dependency 'path', '>= 1.3.0'
14
- s.add_dependency 'nokogiri'
14
+ #s.add_dependency 'nokogiri'
15
15
  s.add_dependency 'rubyzip'
16
16
  s.add_dependency 'term-ansicolor'
17
17
  s.add_dependency 'mime-types', '>= 1.19'
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: storyboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
5
- prerelease:
4
+ version: 0.5.0.pre3
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mark Olson
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-24 00:00:00.000000000 Z
12
+ date: 2013-02-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: nokogiri
15
+ name: bundler
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
@@ -159,6 +159,7 @@ description: Generate PDFs and eBooks from video files
159
159
  email:
160
160
  - ! '"theothermarkolson@gmail.com"'
161
161
  executables:
162
+ - gifboard
162
163
  - storyboard
163
164
  extensions: []
164
165
  extra_rdoc_files: []
@@ -169,11 +170,19 @@ files:
169
170
  - Gemfile
170
171
  - Gemfile.lock
171
172
  - INSTALL.md
173
+ - LICENSE
172
174
  - README.md
175
+ - bin/gifboard
173
176
  - bin/storyboard
177
+ - fonts/DejaVuSans.ttf
178
+ - fonts/KFhimaji.otf
174
179
  - lib/.DS_Store
180
+ - lib/gifboard.rb
175
181
  - lib/storyboard.rb
176
182
  - lib/storyboard/bincheck.rb
183
+ - lib/storyboard/cache.rb
184
+ - lib/storyboard/common.rb
185
+ - lib/storyboard/generators/gif.rb
177
186
  - lib/storyboard/generators/pdf.rb
178
187
  - lib/storyboard/generators/sub.rb
179
188
  - lib/storyboard/subtitles.rb
@@ -221,9 +230,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
230
  required_rubygems_version: !ruby/object:Gem::Requirement
222
231
  none: false
223
232
  requirements:
224
- - - ! '>='
233
+ - - ! '>'
225
234
  - !ruby/object:Gem::Version
226
- version: '0'
235
+ version: 1.3.1
227
236
  requirements: []
228
237
  rubyforge_project:
229
238
  rubygems_version: 1.8.23