mm_tool 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eb195a344b506ce9706580970d5e8db8843bf53b537161d63577dc84577089dc
4
+ data.tar.gz: b06f0eff5f6b0aeddd08b5ac1792d9cdf0869df5babab29fb771f9d3fac28e9d
5
+ SHA512:
6
+ metadata.gz: a68290286f7db0bbcf5a0440c22ad3a213224439530c73280e70426fc52601a2d52d9c9dac278c7558de9b06317b8e284aae78ca6031b7b74f56f3b699c5823c
7
+ data.tar.gz: 769b79d23c56d70d9bbf4fa857629bad1376e332e7b6c87280a86d7d21dd0c54fe029c222a000dd7861c7aa764988372f81fc4e2dc754de3110bd79dd8a0bcb6
data/.gitattributes ADDED
@@ -0,0 +1,4 @@
1
+ *.html linguist-documentation
2
+ *.css linguist-documentation
3
+ *.erb linguist-documentation
4
+ *.js linguist-documentation
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ # Rubymine noise
2
+ .idea/
3
+
4
+ # BBedit noise
5
+ *.bbprojectd
6
+
7
+ # Mac OS X noise
8
+ .DS_Store
9
+
10
+ # Ignore bundler lock files
11
+ Gemfile.lock
12
+
13
+ # Ignore pkg folder
14
+ /pkg
15
+
16
+ # Ignore build folders
17
+ build/*
18
+
19
+ # Yard cruft
20
+ /doc/
21
+ /.yardoc
22
+
23
+ # Aruba
24
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+ ===============
3
+
4
+ Copyright (c) 2020 Jim Derry
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,15 @@
1
+ mm_tool, a multimedia tool
2
+ ==========================
3
+ [![Gem Version](https://badge.fury.io/rb/mm_tool_.svg)](https://badge.fury.io/rb/mm_tool_)
4
+
5
+
6
+ # About
7
+
8
+ tbd
9
+
10
+
11
+ # Change log
12
+
13
+ - 0.1.0
14
+
15
+ - Initial release.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/Truth Tables.xlsx ADDED
Binary file
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mm_tool"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/mm_tool ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #=============================================================================
4
+ # This module consolidates all of the classes the make up a complete MmTool
5
+ # application, and is spread throughout several files based mostly on the
6
+ # classes they contain.
7
+ #=============================================================================
8
+ module MmTool
9
+ require_relative '../lib/mm_tool'
10
+ end
11
+
12
+
13
+ #=============================================================================
14
+ # Main
15
+ #=============================================================================
16
+
17
+ cli = MmTool::MmToolCli.new(MmTool::ApplicationMain.shared_application)
18
+ cli.validate_prerequisites
19
+ cli.run(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/mm_tool.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Setup our load paths
2
+ libdir = File.expand_path(File.dirname(__FILE__))
3
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
4
+
5
+ require "mm_tool/output_helper"
6
+ require "mm_tool/user_defaults"
7
+ require "mm_tool/version"
8
+
9
+ require "mm_tool/application_main"
10
+ require "mm_tool/mm_tool_cli"
11
+
12
+ require 'mm_tool/mm_movie'
13
+ require 'mm_tool/mm_movie_stream'
14
+
15
+ require 'mm_tool/mm_movie_ignore_list'
16
+ require "mm_tool/mm_user_defaults"
17
+
18
+ module MmTool
19
+ class Error < StandardError
20
+ end
21
+ end
@@ -0,0 +1,172 @@
1
+ module MmTool
2
+
3
+ #=============================================================================
4
+ # The main application.
5
+ #=============================================================================
6
+ class ApplicationMain
7
+
8
+ require 'mm_tool.rb'
9
+
10
+ #------------------------------------------------------------
11
+ # Attributes
12
+ #------------------------------------------------------------
13
+ attr_accessor :tempfile
14
+
15
+ #------------------------------------------------------------
16
+ # Initialize
17
+ #------------------------------------------------------------
18
+ def initialize
19
+ @tempfile = nil
20
+ @defaults = MmUserDefaults.shared_user_defaults
21
+ end
22
+
23
+ #------------------------------------------------------------
24
+ # Singleton accessor
25
+ #------------------------------------------------------------
26
+ def self.shared_application
27
+ unless @self
28
+ @self = self.new
29
+ end
30
+ @self
31
+ end
32
+
33
+ #------------------------------------------------------------
34
+ # Output a message to the screen, and if applicable, to
35
+ # the temporary file for later opening.
36
+ #------------------------------------------------------------
37
+ def output(message, command = false)
38
+ puts message
39
+ if @tempfile
40
+ if command
41
+ message = message + "\n"
42
+ else
43
+ message = message.lines.collect {|line| "# #{line}"}.join + "\n"
44
+ end
45
+ @tempfile&.write(message)
46
+ end
47
+ end
48
+
49
+ #------------------------------------------------------------
50
+ # Return the transcode file header.
51
+ #------------------------------------------------------------
52
+ def transcode_script_header
53
+ <<~HEREDOC
54
+ #!/bin/sh
55
+
56
+ # Check this file, make any changes, and save it. It will be executed as soon
57
+ # as you close it. By default, this script will exit per the exit command
58
+ # below. Please further confirm that you wish to proceed with the proposed
59
+ # actions by commenting or removing the line below.
60
+
61
+ exit 1
62
+
63
+ HEREDOC
64
+ end
65
+
66
+ #------------------------------------------------------------
67
+ # Return the report header.
68
+ #------------------------------------------------------------
69
+ #noinspection RubyResolve
70
+ def information_header
71
+ info_table_src = TTY::Table.new
72
+ @defaults.label_value_pairs.each {|pair| info_table_src << pair}
73
+ info_table_src << ["Disposition Columns:", MmMovie.dispositions.join(', ')]
74
+ info_table_src << ["Transcode File Location:", self.tempfile ? tempfile&.path : 'n/a']
75
+
76
+ info_table = C.bold("Looking for file(s) and processing them with the following options:\n")
77
+ info_table << info_table_src.render(:basic) do |renderer|
78
+ a = @defaults.label_value_pairs.map {|p| p[0].length }.max + 1
79
+ b = OutputHelper.console_width - a - 2
80
+ renderer.alignments = [:right, :left]
81
+ renderer.multiline = true
82
+ renderer.column_widths = [a,b]
83
+ end << "\n\n"
84
+ end
85
+
86
+ #------------------------------------------------------------
87
+ # The main run loop, to be run for each file.
88
+ #------------------------------------------------------------
89
+ def run_loop(file_name)
90
+
91
+ if @defaults[:ignore_files]
92
+ MmMovieIgnoreList.shared_ignore_list.add(path: file_name)
93
+ output("Note: added #{file_name} to the list of files to be ignored.")
94
+ elsif @defaults[:unignore_files]
95
+ MmMovieIgnoreList.shared_ignore_list.remove(path: file_name)
96
+ output("Note: removed #{file_name} to the list of files to be ignored.")
97
+ else
98
+ @file_count[:processed] = @file_count[:processed] + 1
99
+ movie = MmMovie.new(with_file: file_name)
100
+ a = movie.interesting?
101
+ b = MmMovieIgnoreList.shared_ignore_list.include?(file_name)
102
+ c = movie.low_quality?
103
+ s = @defaults[:scan_type]&.to_sym
104
+
105
+ if (s == :normal && a && !b && !c) ||
106
+ (s == :all && !b) ||
107
+ (s == :flagged && b) ||
108
+ (s == :quality && c ) ||
109
+ (s == :force)
110
+
111
+ @file_count[:displayed] = @file_count[:displayed] + 1
112
+ output(file_name)
113
+ output(movie.format_table)
114
+ output(movie.stream_table)
115
+ output("#{movie.command_rename} ; \\", true)
116
+ output("#{movie.command_transcode} ; \\", true)
117
+ output(movie.command_review_post, true)
118
+ output("\n\n", true)
119
+ end
120
+ end # if
121
+ end
122
+
123
+ #------------------------------------------------------------
124
+ # Run the application with the given file/directory.
125
+ #------------------------------------------------------------
126
+ def run(file_or_dir)
127
+
128
+ @file_count = { :processed => 0, :displayed => 0 }
129
+
130
+ if @defaults[:transcode]
131
+ @tempfile = Tempfile.create(['mm_tool-', '.sh'])
132
+ @tempfile&.write(transcode_script_header)
133
+ # @tempfile.flush
134
+ end
135
+
136
+ if @defaults[:info_header]
137
+ output information_header
138
+ end
139
+
140
+ if File.file?(file_or_dir)
141
+
142
+ original_scan_type = @defaults[:scan_type]
143
+ @defaults[:scan_type] = :force
144
+ run_loop(file_or_dir)
145
+ @defaults[:scan_type] = original_scan_type
146
+
147
+ elsif File.directory?(file_or_dir)
148
+
149
+ extensions = @defaults[:container_files]&.join(',')
150
+ Dir.chdir(file_or_dir) do
151
+ Dir.glob("**/*.{#{extensions}}").map {|path| File.expand_path(path) }.sort.each do |file|
152
+ run_loop(file)
153
+ end
154
+ end
155
+
156
+ else
157
+ output "Error: Execution should never have reached this point."
158
+ exit 1
159
+ end
160
+
161
+ output("#{File.basename($0)} processed #{@file_count[:processed]} files and displayed data for #{@file_count[:displayed]} of them.")
162
+
163
+ ensure
164
+ if @tempfile
165
+ @tempfile&.close
166
+ # @tempfile.unlink
167
+ end
168
+ end # run
169
+
170
+ end # class
171
+
172
+ end # module
@@ -0,0 +1,217 @@
1
+ module MmTool
2
+
3
+ #=============================================================================
4
+ # A movie as a self contained class. Instances of this class consist of
5
+ # one or more MmMovieStream instances, and contains intelligence about itself
6
+ # so that it can provide commands to ffmpeg and/or mkvpropedit as required.
7
+ # Upon creation it must be provided with a filename.
8
+ #=============================================================================
9
+ class MmMovie
10
+
11
+ require 'streamio-ffmpeg'
12
+ require 'tty-table'
13
+ require 'bytesize'
14
+
15
+ #------------------------------------------------------------
16
+ # This class method returns the known dispositions supported
17
+ # by ffmpeg. This array also reflects the output orders in
18
+ # the dispositions table field. Not combining them would
19
+ # result in a too-long table row.
20
+ #------------------------------------------------------------
21
+ def self.dispositions
22
+ %i(default dub original comment lyrics karaoke forced hearing_impaired visual_impaired clean_effects attached_pic timed_thumbnails)
23
+ end
24
+
25
+ #------------------------------------------------------------
26
+ # Initialize
27
+ #------------------------------------------------------------
28
+ def initialize(with_file:)
29
+ @defaults = MmUserDefaults.shared_user_defaults
30
+ @streams = MmMovieStream::streams(with_files: all_paths(with_file: with_file))
31
+ @format_metadata = FFMPEG::Movie.new(with_file).metadata[:format]
32
+ end
33
+
34
+ #------------------------------------------------------------
35
+ # Get the file-level 'duration' metadata.
36
+ #------------------------------------------------------------
37
+ def format_duration
38
+ seconds = @format_metadata[:duration]
39
+ seconds ? Time.at(seconds.to_i).utc.strftime("%H:%M:%S") : nil
40
+ end
41
+
42
+ #------------------------------------------------------------
43
+ # Get the file-level 'size' metadata.
44
+ #------------------------------------------------------------
45
+ def format_size
46
+ size = @format_metadata[:size]
47
+ size ? ByteSize.new(size) : 'unknown'
48
+ end
49
+
50
+ #------------------------------------------------------------
51
+ # Get the file-level 'title' metadata.
52
+ #------------------------------------------------------------
53
+ def format_title
54
+ @format_metadata&.dig(:tags, :title)
55
+ end
56
+
57
+ #------------------------------------------------------------
58
+ # Indicates whether any of the streams are of a lower
59
+ # quality than desired by the user.
60
+ #------------------------------------------------------------
61
+ def low_quality?
62
+ @streams.count {|stream| stream.low_quality?} > 0
63
+ end
64
+
65
+ #------------------------------------------------------------
66
+ # Indicates whether any of the streams are interesting.
67
+ #------------------------------------------------------------
68
+ def interesting?
69
+ @streams.count {|stream| stream.interesting?} > 0 || format_title
70
+ end
71
+
72
+ #------------------------------------------------------------
73
+ # Get the rendered text of the format_table.
74
+ #------------------------------------------------------------
75
+ def format_table
76
+ unless @format_table
77
+ @format_table = format_table_datasource.render(:basic) do |renderer|
78
+ renderer.column_widths = [10,10, 160]
79
+ renderer.multiline = true
80
+ renderer.padding = [0,1]
81
+ renderer.width = 1000
82
+ end
83
+ end
84
+ @format_table
85
+ end
86
+
87
+ #------------------------------------------------------------
88
+ # For the given table, get the rendered text of the table
89
+ # for output.
90
+ #------------------------------------------------------------
91
+ def stream_table
92
+ unless @stream_table
93
+ @stream_table = stream_table_datasource.render(:unicode) do |renderer|
94
+ renderer.alignments = [:center, :left, :left, :right, :right, :left, :left, :left, :left]
95
+ renderer.column_widths = [5,10,10,5,10,5,23,50,35]
96
+ renderer.multiline = true
97
+ renderer.padding = [0,1]
98
+ renderer.width = 1000
99
+ end # do
100
+ end
101
+ @stream_table
102
+ end
103
+
104
+ #------------------------------------------------------------
105
+ # The complete command to rename the main input file to
106
+ # include a tag indicating that it's the original.
107
+ #------------------------------------------------------------
108
+ def command_rename
109
+ src = @streams[0].source_file
110
+ dst = File.join(File.dirname(src), File.basename(src, '.*') + @defaults[:suffix] + File.extname(src))
111
+ "mv \"#{src}\" \"#{dst}\""
112
+ end
113
+
114
+ #------------------------------------------------------------
115
+ # The complete, proposed ffmpeg command to transcode the
116
+ # input file to an output file. It uses the 'new_input_path'
117
+ # as the input file.
118
+ #------------------------------------------------------------
119
+ def command_transcode
120
+ command = ["ffmpeg \\"]
121
+ @streams.each {|stream| command |= [" #{stream.instruction_input}"] if stream.instruction_input }
122
+ @streams.each {|stream| command << " #{stream.instruction_map}" if stream.instruction_map }
123
+ @streams.each {|stream| command << " #{stream.instruction_action}" if stream.instruction_action }
124
+ @streams.each {|stream| command << " #{stream.instruction_disposition}" if stream.instruction_disposition }
125
+ @streams.each {|stream| command << " #{stream.instruction_metadata}" if stream.instruction_metadata }
126
+ command << " -metadata title=\"#{format_title}\" \\" if format_title
127
+ command << " \"#{output_path}\""
128
+ command.join("\n")
129
+ end
130
+
131
+ #------------------------------------------------------------
132
+ # The complete command to view the output file after
133
+ # running the transcode command
134
+ #------------------------------------------------------------
135
+ def command_review_post
136
+ "\"#{$0}\" --no-use-external-subs \"#{output_path}\""
137
+ end
138
+
139
+
140
+ #============================================================
141
+ private
142
+ #============================================================
143
+
144
+ #------------------------------------------------------------
145
+ # Given the initial file, return an array of the initial
146
+ # file and associated SRTs, which are valid if they have
147
+ # no language, or a language specified in options.
148
+ #------------------------------------------------------------
149
+ def all_paths(with_file:)
150
+ all_paths = [with_file]
151
+ if @defaults[:use_external_subs]
152
+ base_path = File.join(File.dirname(with_file), File.basename(with_file, '.*'))
153
+ all_paths |= ([""] | @defaults[:keep_langs_subs]&.map {|lang| ".#{lang}" })
154
+ .select {|lang| File.file?("#{base_path}#{lang}.srt")}
155
+ .map {|lang| "#{base_path}#{lang}.srt"}
156
+ end
157
+ all_paths
158
+ end
159
+
160
+ #------------------------------------------------------------
161
+ # The name of the proposed output file, if different from
162
+ # the input file. This would be set in the event that the
163
+ # container of the input file is not one of the approved
164
+ # containers.
165
+ #------------------------------------------------------------
166
+ def output_path
167
+ path = @streams[0].source_file
168
+ if @defaults[:containers_preferred]&.include?(File.extname(path))
169
+ path
170
+ else
171
+ File.join(File.dirname(path), File.basename(path, '.*') + '.' + @defaults[:containers_preferred][0])
172
+ end
173
+ end
174
+
175
+ #------------------------------------------------------------
176
+ # Return a TTY::Table of the relevant format data.
177
+ #------------------------------------------------------------
178
+ def format_table_datasource
179
+ unless @format_table
180
+ @format_table = TTY::Table.new(header: %w(Duration: Size: Title:))
181
+ @format_table << [format_duration, format_size, format_title]
182
+ end
183
+ @format_table
184
+ end
185
+
186
+ #------------------------------------------------------------
187
+ # Return a TTY::Table of the movie, populated with the
188
+ # pertinent data of each stream.
189
+ #------------------------------------------------------------
190
+ def stream_table_datasource
191
+ unless @table
192
+ # Ensure that when we add the headers, they specifically are left-aligned.
193
+ headers = %w(index codec type w/# h/layout lang disposition title action(s))
194
+ .map { |header| {:value => header, :alignment => :left} }
195
+
196
+ @table = TTY::Table.new(header: headers)
197
+
198
+ @streams.each do |stream|
199
+ row = []
200
+ row << stream.input_specifier
201
+ row << stream.codec_name
202
+ row << stream.codec_type
203
+ row << stream.quality_01
204
+ row << stream.quality_02
205
+ row << stream.language
206
+ row << stream.dispositions
207
+ row << stream.title
208
+ row << stream.action_label
209
+ @table << row
210
+ end
211
+ end
212
+ @table
213
+ end # table
214
+
215
+ end # class
216
+
217
+ end # module