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 +2 -4
- data/LICENSE +7 -0
- data/README.md +4 -5
- data/bin/gifboard +100 -0
- data/fonts/DejaVuSans.ttf +0 -0
- data/fonts/KFhimaji.otf +0 -0
- data/lib/gifboard.rb +106 -0
- data/lib/storyboard.rb +39 -3
- data/lib/storyboard/cache.rb +56 -0
- data/lib/storyboard/common.rb +6 -0
- data/lib/storyboard/generators/gif.rb +41 -0
- data/lib/storyboard/generators/pdf.rb +1 -1
- data/lib/storyboard/generators/sub.rb +44 -30
- data/lib/storyboard/subtitles.rb +89 -33
- data/lib/storyboard/time.rb +4 -3
- data/lib/storyboard/version.rb +1 -1
- data/storyboard.gemspec +1 -1
- data/vendor/suby/lib/suby/downloader.rb +0 -3
- data/vendor/suby/lib/suby/downloader/opensubtitles.rb +6 -5
- data/vendor/suby/suby.gemspec +1 -1
- metadata +15 -6
data/Gemfile.lock
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
storyboard (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
|
-
|
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
|
data/fonts/KFhimaji.otf
ADDED
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,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
|
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
data/lib/storyboard/subtitles.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
108
|
-
!page[:lines].grep(
|
109
|
-
!page[:lines].grep(
|
110
|
-
!page[:lines].grep(
|
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
|
data/lib/storyboard/time.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
class STRTime
|
2
|
-
REGEX =
|
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|
|
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
|
data/lib/storyboard/version.rb
CHANGED
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 '
|
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
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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)
|
data/vendor/suby/suby.gemspec
CHANGED
@@ -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.
|
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-
|
12
|
+
date: 2013-02-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
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:
|
235
|
+
version: 1.3.1
|
227
236
|
requirements: []
|
228
237
|
rubyforge_project:
|
229
238
|
rubygems_version: 1.8.23
|