video_transcoding 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +573 -0
- data/bin/convert-video +290 -0
- data/bin/detect-crop +130 -0
- data/bin/query-handbrake-log +248 -0
- data/bin/transcode-video +1205 -0
- data/lib/video_transcoding.rb +21 -0
- data/lib/video_transcoding/cli.rb +53 -0
- data/lib/video_transcoding/console.rb +46 -0
- data/lib/video_transcoding/copyright.rb +9 -0
- data/lib/video_transcoding/crop.rb +110 -0
- data/lib/video_transcoding/errors.rb +10 -0
- data/lib/video_transcoding/ffmpeg.rb +47 -0
- data/lib/video_transcoding/handbrake.rb +56 -0
- data/lib/video_transcoding/media.rb +323 -0
- data/lib/video_transcoding/mkvmerge.rb +35 -0
- data/lib/video_transcoding/mkvpropedit.rb +35 -0
- data/lib/video_transcoding/mp4track.rb +35 -0
- data/lib/video_transcoding/mplayer.rb +33 -0
- data/lib/video_transcoding/tool.rb +52 -0
- data/lib/video_transcoding/version.rb +9 -0
- data/video_transcoding.gemspec +22 -0
- metadata +73 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
#
|
2
|
+
# video_transcoding.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'English'
|
8
|
+
require 'shellwords'
|
9
|
+
require 'video_transcoding/console'
|
10
|
+
require 'video_transcoding/copyright'
|
11
|
+
require 'video_transcoding/crop'
|
12
|
+
require 'video_transcoding/errors'
|
13
|
+
require 'video_transcoding/ffmpeg'
|
14
|
+
require 'video_transcoding/handbrake'
|
15
|
+
require 'video_transcoding/media'
|
16
|
+
require 'video_transcoding/mkvmerge'
|
17
|
+
require 'video_transcoding/mkvpropedit'
|
18
|
+
require 'video_transcoding/mp4track'
|
19
|
+
require 'video_transcoding/mplayer'
|
20
|
+
require 'video_transcoding/tool'
|
21
|
+
require 'video_transcoding/version'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#
|
2
|
+
# cli.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'optparse'
|
8
|
+
require 'video_transcoding'
|
9
|
+
|
10
|
+
module VideoTranscoding
|
11
|
+
module CLI
|
12
|
+
def run
|
13
|
+
begin
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
define_options opts
|
16
|
+
opts.on('-v', '--verbose') { Console.volume += 1 }
|
17
|
+
opts.on('-q', '--quiet') { Console.volume -= 1 }
|
18
|
+
|
19
|
+
opts.on '-h', '--help' do
|
20
|
+
puts usage
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on '--version' do
|
25
|
+
puts about
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
end.parse!
|
29
|
+
rescue OptionParser::ParseError => e
|
30
|
+
raise UsageError, e
|
31
|
+
end
|
32
|
+
|
33
|
+
Console.info about
|
34
|
+
configure
|
35
|
+
fail UsageError, 'missing argument' if ARGV.empty?
|
36
|
+
ARGV.each { |arg| process_input arg }
|
37
|
+
complete if self.respond_to? :complete
|
38
|
+
Console.info 'Done.'
|
39
|
+
terminate if self.respond_to? :terminate
|
40
|
+
exit
|
41
|
+
rescue UsageError => e
|
42
|
+
warn "#{$PROGRAM_NAME}: #{e}\nTry `#{$PROGRAM_NAME} --help` for more information."
|
43
|
+
exit false
|
44
|
+
rescue StandardError => e
|
45
|
+
Console.error "#{$PROGRAM_NAME}: #{e}"
|
46
|
+
exit(-1)
|
47
|
+
rescue SignalException
|
48
|
+
terminate if self.respond_to? :terminate
|
49
|
+
puts
|
50
|
+
exit(-1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# console.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
module VideoTranscoding
|
8
|
+
module Console
|
9
|
+
OFF = 0
|
10
|
+
ERROR = 1
|
11
|
+
WARN = 2
|
12
|
+
INFO = 3
|
13
|
+
DEBUG = 4
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
fail "#{self} singleton can't be included by #{base}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.extended(base)
|
20
|
+
fail "#{self} singleton can't be extended by #{base}" unless base.class == Module
|
21
|
+
@speaker = ->(msg) { Kernel.warn msg }
|
22
|
+
@volume = WARN
|
23
|
+
end
|
24
|
+
|
25
|
+
extend self
|
26
|
+
|
27
|
+
attr_accessor :speaker
|
28
|
+
attr_accessor :volume
|
29
|
+
|
30
|
+
def error(msg)
|
31
|
+
@speaker.call msg unless @speaker.nil? or @volume < ERROR
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn(msg)
|
35
|
+
@speaker.call msg unless @speaker.nil? or @volume < WARN
|
36
|
+
end
|
37
|
+
|
38
|
+
def info(msg)
|
39
|
+
@speaker.call msg unless @speaker.nil? or @volume < INFO
|
40
|
+
end
|
41
|
+
|
42
|
+
def debug(msg)
|
43
|
+
@speaker.call msg unless @speaker.nil? or @volume < DEBUG
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# crop.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
module VideoTranscoding
|
8
|
+
module Crop
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def detect(path, duration, width, height)
|
12
|
+
Console.info "Detecting crop with mplayer..."
|
13
|
+
fail "media duration too short: #{duration} second(s)" if duration < 2
|
14
|
+
steps = 10
|
15
|
+
interval = duration / (steps + 1)
|
16
|
+
target_interval = 5 * 60
|
17
|
+
|
18
|
+
if interval == 0
|
19
|
+
steps = 1
|
20
|
+
interval = 1
|
21
|
+
elsif interval > target_interval
|
22
|
+
steps = (duration / target_interval) - 1
|
23
|
+
interval = duration / (steps + 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
crop_width, crop_height, crop_x, crop_y = 0, 0, width, height
|
27
|
+
last_seconds = Time.now.tv_sec
|
28
|
+
|
29
|
+
(1..steps).each do |step|
|
30
|
+
begin
|
31
|
+
IO.popen([
|
32
|
+
MPlayer.command_name,
|
33
|
+
'-quiet',
|
34
|
+
'-benchmark',
|
35
|
+
'-vo', 'null',
|
36
|
+
'-ao', 'null',
|
37
|
+
'-vf', 'cropdetect=24:2',
|
38
|
+
path,
|
39
|
+
'-ss', (interval * step).to_s,
|
40
|
+
'-frames', '10'
|
41
|
+
], :err=>[:child, :out]) do |io|
|
42
|
+
io.each do |line|
|
43
|
+
seconds = Time.now.tv_sec
|
44
|
+
|
45
|
+
if seconds - last_seconds >= 3
|
46
|
+
Console.warn '...'
|
47
|
+
last_seconds = seconds
|
48
|
+
end
|
49
|
+
|
50
|
+
if line =~ /^\[CROP\] .* crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)/
|
51
|
+
d_width, d_height, d_x, d_y = $1.to_i, $2.to_i, $3.to_i, $4.to_i
|
52
|
+
crop_width = d_width if crop_width < d_width
|
53
|
+
crop_height = d_height if crop_height < d_height
|
54
|
+
crop_x = d_x if crop_x > d_x
|
55
|
+
crop_y = d_y if crop_y > d_y
|
56
|
+
Console.debug line
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
rescue SystemCallError => e
|
61
|
+
raise "crop detection failed: #{e}"
|
62
|
+
end
|
63
|
+
|
64
|
+
fail "crop detection failed" unless $CHILD_STATUS.exitstatus == 0
|
65
|
+
end
|
66
|
+
|
67
|
+
{
|
68
|
+
:top => crop_y,
|
69
|
+
:bottom => height - (crop_y + crop_height),
|
70
|
+
:left => crop_x,
|
71
|
+
:right => width - (crop_x + crop_width)
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def constrain(raw_crop, width, height)
|
76
|
+
crop = raw_crop.dup
|
77
|
+
delta_x = crop[:left] + crop[:right]
|
78
|
+
delta_y = crop[:top] + crop[:bottom]
|
79
|
+
min_delta = (width / 64)
|
80
|
+
min_delta += 1 if min_delta.odd?
|
81
|
+
|
82
|
+
if delta_x > delta_y
|
83
|
+
crop[:top], crop[:bottom] = 0, 0
|
84
|
+
|
85
|
+
if delta_x < min_delta or delta_x >= width
|
86
|
+
crop[:left], crop[:right] = 0, 0
|
87
|
+
end
|
88
|
+
else
|
89
|
+
crop[:left], crop[:right] = 0, 0
|
90
|
+
|
91
|
+
if delta_y < min_delta or delta_y >= height
|
92
|
+
crop[:top], crop[:bottom] = 0, 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
crop
|
97
|
+
end
|
98
|
+
|
99
|
+
def handbrake_string(crop)
|
100
|
+
"#{crop[:top]}:#{crop[:bottom]}:#{crop[:left]}:#{crop[:right]}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def mplayer_string(crop, width, height)
|
104
|
+
"#{width - (crop[:left] + crop[:right])}:" +
|
105
|
+
"#{height - (crop[:top] + crop[:bottom])}:" +
|
106
|
+
"#{crop[:left]}:" +
|
107
|
+
"#{crop[:top]}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#
|
2
|
+
# ffmpeg.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
module VideoTranscoding
|
8
|
+
module FFmpeg
|
9
|
+
extend self
|
10
|
+
|
11
|
+
COMMAND_NAME = 'ffmpeg'
|
12
|
+
|
13
|
+
def setup
|
14
|
+
Tool.provide(COMMAND_NAME, ['-version']) do |output, status, properties|
|
15
|
+
fail "#{COMMAND_NAME} failed during execution" unless status == 0
|
16
|
+
|
17
|
+
unless output =~ /^ffmpeg version ([0-9.]+)/
|
18
|
+
Console.debug output
|
19
|
+
fail "#{COMMAND_NAME} version unknown"
|
20
|
+
end
|
21
|
+
|
22
|
+
version = $1
|
23
|
+
Console.info "#{$MATCH} found..."
|
24
|
+
|
25
|
+
unless version =~ /^([0-9]+)\.([0-9]+)/ and (($1.to_i * 100) + $2.to_i) >= 205
|
26
|
+
fail "#{COMMAND_NAME} version 2.5.0 or later required"
|
27
|
+
end
|
28
|
+
|
29
|
+
if output =~ /--enable-libfdk-aac/
|
30
|
+
properties[:aac_encoder] = 'libfdk_aac'
|
31
|
+
elsif output =~ /--enable-libfaac/
|
32
|
+
properties[:aac_encoder] = 'libfaac'
|
33
|
+
else
|
34
|
+
properties[:aac_encoder] = 'aac'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def command_name
|
40
|
+
Tool.use(COMMAND_NAME)
|
41
|
+
end
|
42
|
+
|
43
|
+
def aac_encoder
|
44
|
+
Tool.properties(COMMAND_NAME)[:aac_encoder]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#
|
2
|
+
# handbrake.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
module VideoTranscoding
|
8
|
+
module HandBrake
|
9
|
+
extend self
|
10
|
+
|
11
|
+
COMMAND_NAME = 'HandBrakeCLI'
|
12
|
+
|
13
|
+
def setup
|
14
|
+
Tool.provide(COMMAND_NAME, ['--preset-list']) do |output, status, _|
|
15
|
+
fail "#{COMMAND_NAME} failed during execution" unless status == 0
|
16
|
+
|
17
|
+
unless output =~ /^HandBrake ([^ ]+)/
|
18
|
+
Console.debug output
|
19
|
+
fail "#{COMMAND_NAME} version unknown"
|
20
|
+
end
|
21
|
+
|
22
|
+
version = $1
|
23
|
+
Console.info "#{$MATCH} found..."
|
24
|
+
|
25
|
+
unless (version =~ /^([0-9]+)\.([0-9]+)/ and (($1.to_i * 100) + $2.to_i) >= 10) or
|
26
|
+
(version =~ /^svn([0-9]+)/ and $1.to_i >= 6536)
|
27
|
+
fail "#{COMMAND_NAME} version 0.10.0 or later required"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def command_name
|
33
|
+
Tool.use(COMMAND_NAME)
|
34
|
+
end
|
35
|
+
|
36
|
+
def aac_encoder
|
37
|
+
properties = Tool.properties(COMMAND_NAME)
|
38
|
+
return properties[:aac_encoder] if properties.has_key? :aac_encoder
|
39
|
+
output = ''
|
40
|
+
|
41
|
+
begin
|
42
|
+
IO.popen([Tool.use(COMMAND_NAME), '--help'], :err=>[:child, :out]) { |io| output = io.read }
|
43
|
+
rescue SystemCallError => e
|
44
|
+
raise "#{COMMAND_NAME} AAC encoder unknown: #{e}"
|
45
|
+
end
|
46
|
+
|
47
|
+
fail "#{COMMAND_NAME} failed during execution" unless $CHILD_STATUS.exitstatus == 0
|
48
|
+
|
49
|
+
if output =~ /ca_aac/
|
50
|
+
properties[:aac_encoder] = 'ca_aac'
|
51
|
+
else
|
52
|
+
properties[:aac_encoder] = 'av_aac'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
#
|
2
|
+
# media.rb
|
3
|
+
#
|
4
|
+
# Copyright (c) 2013-2015 Don Melton
|
5
|
+
#
|
6
|
+
|
7
|
+
module VideoTranscoding
|
8
|
+
class Media
|
9
|
+
attr_reader :path
|
10
|
+
|
11
|
+
def initialize(path:, title: nil, autocrop: false, extended: true, allow_directory: true)
|
12
|
+
@path = path
|
13
|
+
@previews = autocrop ? 10 : 2
|
14
|
+
@extended = extended
|
15
|
+
@scan = nil
|
16
|
+
@stat = File.stat(@path)
|
17
|
+
|
18
|
+
if @stat.directory?
|
19
|
+
fail UsageError, "not a file: #{@path}" unless allow_directory
|
20
|
+
@title = title
|
21
|
+
|
22
|
+
if @title.nil?
|
23
|
+
@scan = Media.scan(@path, 0, 2)
|
24
|
+
elsif @title < 1
|
25
|
+
fail UsageError, "invalid title index: #{@title}"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
fail UsageError, "invalid title index: #{title}" unless title.nil? or title == 1
|
29
|
+
@title = 1
|
30
|
+
end
|
31
|
+
|
32
|
+
@scan = Media.scan(@path, @title, @previews) if @scan.nil?
|
33
|
+
@first_scan = @scan
|
34
|
+
@mp4_scan = nil
|
35
|
+
@summary = nil
|
36
|
+
@info = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def summary
|
40
|
+
if @summary.nil?
|
41
|
+
@summary = ''
|
42
|
+
@first_scan.each_line { |line| @summary += line if line =~ /^ *\+ (?!(autocrop|support))/ }
|
43
|
+
end
|
44
|
+
|
45
|
+
@summary
|
46
|
+
end
|
47
|
+
|
48
|
+
def info
|
49
|
+
return @info unless @info.nil?
|
50
|
+
@info = {}
|
51
|
+
|
52
|
+
if @title.nil?
|
53
|
+
if @scan =~ /\+ title ([0-9]+):\n \+ Main Feature/m
|
54
|
+
@title = $1.to_i
|
55
|
+
elsif @scan =~ /^\+ title ([0-9]+):$/
|
56
|
+
@title = $1.to_i
|
57
|
+
else
|
58
|
+
@title = 1
|
59
|
+
end
|
60
|
+
|
61
|
+
@scan = Media.scan(@path, @title, @previews)
|
62
|
+
end
|
63
|
+
|
64
|
+
if @scan =~ / libhb: scan thread found ([0-9.]+) valid title/
|
65
|
+
fail 'multiple titles found' if $1.to_i > 1
|
66
|
+
end
|
67
|
+
|
68
|
+
@info[:title] = @title
|
69
|
+
@info[:size] = @stat.size
|
70
|
+
@info[:directory] = @stat.directory?
|
71
|
+
|
72
|
+
unless @scan =~ /^ \+ duration: ([0-9]{2}):([0-9]{2}):([0-9]{2})$/
|
73
|
+
fail 'media duration not found'
|
74
|
+
end
|
75
|
+
|
76
|
+
@info[:duration] = ($1.to_i * 60 * 60) + ($2.to_i * 60) + $3.to_i
|
77
|
+
|
78
|
+
unless @scan =~ / scan: [0-9]+ previews, ([0-9]+)x([0-9]+), ([0-9.]+) fps, autocrop = ([0-9]+)\/([0-9]+)\/([0-9]+)\/([0-9]+), aspect [0-9.]+:[0-9.]+, PAR [0-9]+:[0-9]+/
|
79
|
+
fail 'video information not found'
|
80
|
+
end
|
81
|
+
|
82
|
+
@info[:width] = $1.to_i
|
83
|
+
@info[:height] = $2.to_i
|
84
|
+
@info[:fps] = $3.to_f
|
85
|
+
|
86
|
+
if @previews > 2
|
87
|
+
@info[:autocrop] = {:top => $4.to_i, :bottom => $5.to_i, :left => $6.to_i, :right => $7.to_i}
|
88
|
+
else
|
89
|
+
@info[:autocrop] = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
return @info unless @extended
|
93
|
+
|
94
|
+
unless @scan =~ / \+ audio tracks:\n(.*) \+ subtitle tracks:\n(.*)HandBrake has exited./m
|
95
|
+
fail 'audio and subtitle information not found'
|
96
|
+
end
|
97
|
+
|
98
|
+
audio = $1
|
99
|
+
subtitle = $2
|
100
|
+
@info[:audio] = {}
|
101
|
+
@info[:subtitle] = {}
|
102
|
+
|
103
|
+
audio.each_line do |line|
|
104
|
+
if line =~ /^ \+ ([0-9.]+), [^(]+\(([^)]+)\) .*\(([0-9.]+) ch\) .*\(iso639-2: ([a-z]{3})\)/
|
105
|
+
track = $1.to_i
|
106
|
+
track_info = {}
|
107
|
+
track_info[:format] = $2
|
108
|
+
track_info[:channels] = $3.to_f
|
109
|
+
track_info[:language] = $4
|
110
|
+
|
111
|
+
if line =~ /([0-9]+)bps/
|
112
|
+
track_info[:bps] = $1.to_i
|
113
|
+
else
|
114
|
+
track_info[:bps] = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
@info[:audio][track] = track_info
|
118
|
+
else
|
119
|
+
fail "invalid audio track information: #{line}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
subtitle.each_line do |line|
|
124
|
+
if line =~ /^ \+ ([0-9.]+), .*\(iso639-2: ([a-z]{3})\) \((?:Text|Bitmap)\)\(([^)]+)\)/
|
125
|
+
track = $1.to_i
|
126
|
+
track_info = {}
|
127
|
+
track_info[:language] = $2
|
128
|
+
track_info[:format] = $3
|
129
|
+
@info[:subtitle][track] = track_info
|
130
|
+
else
|
131
|
+
fail "invalid subtitle track information: #{line}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
@info[:h264] = false
|
136
|
+
@info[:mpeg2] = false
|
137
|
+
audio_track = 0
|
138
|
+
subtitle_track = 0
|
139
|
+
|
140
|
+
@scan.each_line do |line|
|
141
|
+
if line =~ /[ ]+Stream #0\.([0-9]+)[^ ]*: (Video|Audio|Subtitle): (.*)/
|
142
|
+
stream = $1.to_i
|
143
|
+
type = $2
|
144
|
+
attributes = $3
|
145
|
+
|
146
|
+
case type
|
147
|
+
when 'Video'
|
148
|
+
unless @info.has_key? :stream
|
149
|
+
@info[:stream] = stream
|
150
|
+
|
151
|
+
if attributes =~ /^h264/
|
152
|
+
@info[:h264] = true
|
153
|
+
elsif attributes =~ /^mpeg2video/
|
154
|
+
@info[:mpeg2] = true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
when 'Audio'
|
158
|
+
audio_track += 1
|
159
|
+
track_info = @info[:audio][audio_track]
|
160
|
+
track_info[:stream] = stream
|
161
|
+
|
162
|
+
if attributes =~ /\(default\)/
|
163
|
+
track_info[:default] = true
|
164
|
+
else
|
165
|
+
track_info[:default] = false
|
166
|
+
end
|
167
|
+
|
168
|
+
if @scan =~ /[ ]+Stream #0\.#{stream}[^ ]*: Audio: [^\n]+\n[ ]+Metadata:\n^[ ]+title[ ]+: ([^\n]+)/m
|
169
|
+
track_info[:name] = $1
|
170
|
+
else
|
171
|
+
track_info[:name] = nil
|
172
|
+
end
|
173
|
+
when 'Subtitle'
|
174
|
+
subtitle_track += 1
|
175
|
+
track_info = @info[:subtitle][subtitle_track]
|
176
|
+
track_info[:stream] = stream
|
177
|
+
|
178
|
+
if attributes =~ /\(default\)/
|
179
|
+
track_info[:default] = true
|
180
|
+
else
|
181
|
+
track_info[:default] = false
|
182
|
+
end
|
183
|
+
|
184
|
+
if attributes =~ /\(forced\)/
|
185
|
+
track_info[:forced] = true
|
186
|
+
else
|
187
|
+
track_info[:forced] = false
|
188
|
+
end
|
189
|
+
|
190
|
+
if @scan =~ /[ ]+Stream #0\.#{stream}[^ ]*: Subtitle: [^\n]+\n[ ]+Metadata:\n^[ ]+title[ ]+: ([^\n]+)/m
|
191
|
+
track_info[:name] = $1
|
192
|
+
else
|
193
|
+
track_info[:name] = nil
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
@info[:bd] = false
|
200
|
+
@info[:dvd] = false
|
201
|
+
|
202
|
+
if @scan =~ / scan: (BD|DVD) has [0-9]+ title/
|
203
|
+
@info[$1.downcase.to_sym] = true
|
204
|
+
@info[:mpeg2] = true if @info[:dvd]
|
205
|
+
end
|
206
|
+
|
207
|
+
if @scan =~ /Input #0, matroska/
|
208
|
+
@info[:mkv] = true
|
209
|
+
else
|
210
|
+
@info[:mkv] = false
|
211
|
+
end
|
212
|
+
|
213
|
+
if @scan =~ /Input #0, mov,mp4/
|
214
|
+
@info[:mp4] = true
|
215
|
+
else
|
216
|
+
@info[:mp4] = false
|
217
|
+
end
|
218
|
+
|
219
|
+
if @info[:mp4]
|
220
|
+
@mp4_scan = Media.scan_mp4(@path)
|
221
|
+
types = []
|
222
|
+
flags = []
|
223
|
+
names = []
|
224
|
+
|
225
|
+
@mp4_scan.each_line do |line|
|
226
|
+
if line =~ /^[ ]+type[ ]+=[ ]+(.*)$/
|
227
|
+
types << $1
|
228
|
+
elsif line =~ /^[ ]+enabled[ ]+=[ ]+(.*)$/
|
229
|
+
if $1 == 'true'
|
230
|
+
flags << true
|
231
|
+
else
|
232
|
+
flags << false
|
233
|
+
end
|
234
|
+
elsif line =~ /^[ ]+userDataName[ ]+=[ ]+(.*)$/
|
235
|
+
if $1 == '<absent>'
|
236
|
+
names << nil
|
237
|
+
else
|
238
|
+
names << $1
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
index = 0
|
244
|
+
audio_track = 0
|
245
|
+
subtitle_track = 0
|
246
|
+
|
247
|
+
types.each do |type|
|
248
|
+
case type
|
249
|
+
when 'audio'
|
250
|
+
audio_track += 1
|
251
|
+
track_info = @info[:audio][audio_track]
|
252
|
+
track_info[:default] = flags[index]
|
253
|
+
track_info[:name] = names[index]
|
254
|
+
when 'text'
|
255
|
+
subtitle_track += 1
|
256
|
+
track_info = @info[:subtitle][subtitle_track]
|
257
|
+
track_info[:default] = flags[index]
|
258
|
+
track_info[:forced] = flags[index]
|
259
|
+
track_info[:name] = names[index]
|
260
|
+
end
|
261
|
+
|
262
|
+
index += 1
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@info
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.scan(path, title, previews)
|
270
|
+
output = ''
|
271
|
+
label = title == 0 ? 'media' : "media title #{title}"
|
272
|
+
Console.info "Scanning #{label} with HandBrakeCLI..."
|
273
|
+
last_seconds = Time.now.tv_sec
|
274
|
+
|
275
|
+
begin
|
276
|
+
IO.popen([
|
277
|
+
HandBrake.command_name,
|
278
|
+
"--title=#{title}",
|
279
|
+
'--scan',
|
280
|
+
"--previews=#{previews}:0",
|
281
|
+
"--input=#{path}"
|
282
|
+
], :err=>[:child, :out]) do |io|
|
283
|
+
io.each do |line|
|
284
|
+
seconds = Time.now.tv_sec
|
285
|
+
|
286
|
+
if seconds - last_seconds >= 3
|
287
|
+
Console.warn '...'
|
288
|
+
last_seconds = seconds
|
289
|
+
end
|
290
|
+
|
291
|
+
line.encode! 'UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''
|
292
|
+
Console.debug line
|
293
|
+
output += line
|
294
|
+
end
|
295
|
+
end
|
296
|
+
rescue SystemCallError => e
|
297
|
+
raise "scanning #{label} failed: #{e}"
|
298
|
+
end
|
299
|
+
|
300
|
+
fail "scanning #{label} failed" unless $CHILD_STATUS.exitstatus == 0
|
301
|
+
output
|
302
|
+
end
|
303
|
+
|
304
|
+
def self.scan_mp4(path)
|
305
|
+
output = ''
|
306
|
+
Console.info 'Scanning with mp4track...'
|
307
|
+
|
308
|
+
begin
|
309
|
+
IO.popen([
|
310
|
+
MP4track.command_name,
|
311
|
+
'--list',
|
312
|
+
path
|
313
|
+
], :err=>[:child, :out]) { |io| output = io.read }
|
314
|
+
rescue SystemCallError => e
|
315
|
+
raise "scanning failed: #{e}"
|
316
|
+
end
|
317
|
+
|
318
|
+
Console.debug output
|
319
|
+
fail 'scanning failed' unless $CHILD_STATUS.exitstatus == 0
|
320
|
+
output
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|