mm_tool 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module MmTool
2
+
3
+ #=============================================================================
4
+ # A list of movie files to ignore during :normal and :all scan types. Will
5
+ # keep the on-file list in sync with the in-memory list.
6
+ #=============================================================================
7
+ class MmMovieIgnoreList
8
+
9
+ require 'fileutils'
10
+ require 'yaml'
11
+
12
+ #------------------------------------------------------------
13
+ # Singleton accessor.
14
+ #------------------------------------------------------------
15
+ def self.shared_ignore_list
16
+ unless @self
17
+ @self = self.new
18
+ end
19
+ @self
20
+ end
21
+
22
+ #------------------------------------------------------------
23
+ # Initialize
24
+ #------------------------------------------------------------
25
+ def initialize
26
+ @ignore_list = []
27
+ if !File.file?(file_path)
28
+ FileUtils.mkdir_p(File.dirname(file_path))
29
+ else
30
+ #noinspection RubyResolve
31
+ @ignore_list = YAML.load(File.read(file_path))
32
+ end
33
+ end
34
+
35
+ #------------------------------------------------------------
36
+ # The location on the filesystem where the file exists.
37
+ #------------------------------------------------------------
38
+ def file_path
39
+ PATH_IGNORE_LIST
40
+ end
41
+
42
+ #------------------------------------------------------------
43
+ # Is the given file on the ignore list?
44
+ #------------------------------------------------------------
45
+ def include?(path)
46
+ @ignore_list.include?(path)
47
+ end
48
+
49
+ #------------------------------------------------------------
50
+ # Add a path to the list, and write list to disk.
51
+ #------------------------------------------------------------
52
+ def add(path:)
53
+ new_list = @ignore_list |= [path]
54
+ @ignore_list = new_list.sort
55
+ File.open(file_path, 'w') { |file| file.write(@ignore_list.to_yaml) }
56
+ end
57
+
58
+ #------------------------------------------------------------
59
+ # Remove a path from the list, and update on disk.
60
+ #------------------------------------------------------------
61
+ def remove(path:)
62
+ @ignore_list.delete(path)
63
+ File.open(file_path, 'w') { |file| file.write(@ignore_list.to_yaml) }
64
+ end
65
+
66
+ end # class
67
+
68
+ end # module
@@ -0,0 +1,492 @@
1
+ module MmTool
2
+
3
+ #=============================================================================
4
+ # A stream of an MmMovie. Instances contain simple accessors to the data
5
+ # made available by ffmpeg, and have knowledge on how to generate useful
6
+ # arguments for ffmpeg and mkvpropedit.
7
+ #=============================================================================
8
+ class MmMovieStream
9
+
10
+ require 'streamio-ffmpeg'
11
+ require 'mm_tool/mm_movie'
12
+
13
+ #------------------------------------------------------------
14
+ # Given an array of related files, this class method returns
15
+ # an array of MmMovieStreams reflecting the streams present
16
+ # in each of them.
17
+ #------------------------------------------------------------
18
+ def self.streams(with_files:)
19
+ # Arrays are passed around by reference; when this array is created and
20
+ # used as a reference in each stream, and *also* returned from this class
21
+ # method, everyone will still be using the same reference. It's important
22
+ # below to build up this array without replacing it with another instance.
23
+ streams = []
24
+ with_files.each_with_index do |path, i|
25
+ ff_movie = FFMPEG::Movie.new(path)
26
+ ff_movie.metadata[:streams].each do |stream|
27
+ streams << MmMovieStream.new(stream_data: stream, source_file: path, file_number: i, streams_ref: streams)
28
+ end
29
+ end
30
+ streams
31
+ end
32
+
33
+ #------------------------------------------------------------
34
+ # Initialize
35
+ #------------------------------------------------------------
36
+ def initialize(stream_data:, source_file:, file_number:, streams_ref:)
37
+ @defaults = MmUserDefaults.shared_user_defaults
38
+ @data = stream_data
39
+ @source_file = source_file
40
+ @file_number = file_number
41
+ @streams = streams_ref
42
+ end
43
+
44
+ #------------------------------------------------------------
45
+ # Attribute accessors
46
+ #------------------------------------------------------------
47
+ attr_accessor :file_number
48
+ attr_accessor :source_file
49
+
50
+ #------------------------------------------------------------
51
+ # Property - returns the index of the stream.
52
+ #------------------------------------------------------------
53
+ def index
54
+ @data[:index]
55
+ end
56
+
57
+ #------------------------------------------------------------
58
+ # Property - returns the input specifier of the stream.
59
+ #------------------------------------------------------------
60
+ def input_specifier
61
+ "#{@file_number}:#{index}"
62
+ end
63
+
64
+ #------------------------------------------------------------
65
+ # Property - returns the codec name of the stream.
66
+ #------------------------------------------------------------
67
+ def codec_name
68
+ @data[:codec_name]
69
+ end
70
+
71
+ #------------------------------------------------------------
72
+ # Property - returns the codec type of the stream.
73
+ #------------------------------------------------------------
74
+ def codec_type
75
+ @data[:codec_type]
76
+ end
77
+
78
+ #------------------------------------------------------------
79
+ # Property - returns the coded width of the stream.
80
+ #------------------------------------------------------------
81
+ def coded_width
82
+ @data[:coded_width]
83
+ end
84
+
85
+ #------------------------------------------------------------
86
+ # Property - returns the coded height of the stream.
87
+ #------------------------------------------------------------
88
+ def coded_height
89
+ @data[:coded_height]
90
+ end
91
+
92
+ #------------------------------------------------------------
93
+ # Property - returns the number of channels of the stream.
94
+ #------------------------------------------------------------
95
+ def channels
96
+ @data[:channels]
97
+ end
98
+
99
+ #------------------------------------------------------------
100
+ # Property - returns the channel layout of the stream.
101
+ #------------------------------------------------------------
102
+ def channel_layout
103
+ @data[:channel_layout]
104
+ end
105
+
106
+ #------------------------------------------------------------
107
+ # Property - returns the language of the stream, or 'und'
108
+ # if the language is not defined.
109
+ #------------------------------------------------------------
110
+ def language
111
+ if @data.key?(:tags)
112
+ lang = @data[:tags][:language]
113
+ lang = @data[:tags][:LANGUAGE] unless lang
114
+ lang = 'und' unless lang
115
+ else
116
+ lang = 'und'
117
+ end
118
+ lang
119
+ end
120
+
121
+ #------------------------------------------------------------
122
+ # Property - returns the title of the stream, or nil.
123
+ #------------------------------------------------------------
124
+ def title
125
+ if @data.key?(:tags)
126
+ @data[:tags][:title]
127
+ else
128
+ nil
129
+ end
130
+ end
131
+
132
+ #------------------------------------------------------------
133
+ # Property - returns the disposition flags of the stream as
134
+ # a comma-separated list for compactness.
135
+ #------------------------------------------------------------
136
+ def dispositions
137
+ MmMovie.dispositions
138
+ .collect {|symbol| @data[:disposition][symbol]}
139
+ .join(',')
140
+ end
141
+
142
+ #------------------------------------------------------------
143
+ # Property - returns an appropriate "quality" indicator
144
+ # based on the type of the stream.
145
+ #------------------------------------------------------------
146
+ def quality_01
147
+ if codec_type == 'audio'
148
+ channels
149
+ elsif codec_type == 'video'
150
+ coded_width
151
+ else
152
+ nil
153
+ end
154
+ end
155
+
156
+ #------------------------------------------------------------
157
+ # Property - returns a different appropriate "quality"
158
+ # indicator based on the type of the stream.
159
+ #------------------------------------------------------------
160
+ def quality_02
161
+ if codec_type == 'audio'
162
+ channel_layout
163
+ elsif codec_type == 'video'
164
+ coded_height
165
+ else
166
+ nil
167
+ end
168
+ end
169
+
170
+ #------------------------------------------------------------
171
+ # Property - returns a convenient label indicating the
172
+ # recommended actions for the stream.
173
+ #------------------------------------------------------------
174
+ def action_label
175
+ "#{output_specifier} #{actions.select {|a| a != :interesting}.join(' ')}"
176
+ end
177
+
178
+ #------------------------------------------------------------
179
+ # Property - indicates whether or not the stream is the
180
+ # default stream per its dispositions.
181
+ #------------------------------------------------------------
182
+ def default?
183
+ @data[:disposition][:default] == 1
184
+ end
185
+
186
+ #------------------------------------------------------------
187
+ # Property - indicates whether or not the stream is
188
+ # considered "low quality" based on the application
189
+ # configuration.
190
+ #------------------------------------------------------------
191
+ def low_quality?
192
+ if codec_type == 'audio'
193
+ channels.to_i < @defaults[:min_channels].to_i
194
+ elsif codec_type == 'video'
195
+ coded_width.to_i < @defaults[:min_width].to_i
196
+ else
197
+ false
198
+ end
199
+ end
200
+
201
+ #------------------------------------------------------------
202
+ # Property - stream action includes :drop?
203
+ #------------------------------------------------------------
204
+ def drop?
205
+ actions.include?(:drop)
206
+ end
207
+
208
+ #------------------------------------------------------------
209
+ # Property - stream action includes :copy?
210
+ #------------------------------------------------------------
211
+ def copy?
212
+ actions.include?(:copy)
213
+ end
214
+
215
+ #------------------------------------------------------------
216
+ # Property - stream action includes :transcode?
217
+ #------------------------------------------------------------
218
+ def transcode?
219
+ actions.include?(:transcode)
220
+ end
221
+
222
+ #------------------------------------------------------------
223
+ # Property - stream action includes :set_language?
224
+ #------------------------------------------------------------
225
+ def set_language?
226
+ actions.include?(:set_language)
227
+ end
228
+
229
+ #------------------------------------------------------------
230
+ # Property - stream action includes :interesting?
231
+ #------------------------------------------------------------
232
+ def interesting?
233
+ actions.include?(:interesting)
234
+ end
235
+
236
+ #------------------------------------------------------------
237
+ # Property - indicates whether or not the stream will be
238
+ # unique for its type at output.
239
+ #------------------------------------------------------------
240
+ def output_unique?
241
+ @streams.count {|s| s.codec_type == codec_type && !s.drop? } == 1
242
+ end
243
+
244
+ #------------------------------------------------------------
245
+ # Property - indicates whether or not this stream is the
246
+ # only one of its type.
247
+ #------------------------------------------------------------
248
+ def one_of_a_kind?
249
+ @streams.count {|s| s.codec_type == codec_type && s != self } == 0
250
+ end
251
+
252
+ #------------------------------------------------------------
253
+ # Property - returns the index of the stream in the output
254
+ # file.
255
+ #------------------------------------------------------------
256
+ def output_index
257
+ @streams.select {|s| !s.drop? }.index(self)
258
+ end
259
+
260
+ #------------------------------------------------------------
261
+ # Property - returns a specific output specifier for the
262
+ # stream, such as v:0 or a:2.
263
+ #------------------------------------------------------------
264
+ def output_specifier
265
+ idx = @streams.select {|s| s.codec_type == codec_type && !s.drop?}.index(self)
266
+ idx ? "#{codec_type[0]}:#{idx}" : ' ⬇ '
267
+ end
268
+
269
+ #------------------------------------------------------------
270
+ # Property - returns the -i input instruction for this
271
+ # stream.
272
+ #------------------------------------------------------------
273
+ def instruction_input
274
+ src = if @file_number == 0
275
+ File.join(File.dirname(@source_file), File.basename(@source_file, '.*') + @defaults[:suffix] + File.extname(@source_file))
276
+ else
277
+ @source_file
278
+ end
279
+ "-i \"#{src}\" \\"
280
+ end
281
+
282
+ #------------------------------------------------------------
283
+ # Property - returns the -map instruction for this stream,
284
+ # according to the action(s) determined.
285
+ #------------------------------------------------------------
286
+ def instruction_map
287
+ drop? ? nil : "-map #{input_specifier} \\"
288
+ end
289
+
290
+ #------------------------------------------------------------
291
+ # Property - returns an instruction for handling the stream,
292
+ # according to the action(s) determined.
293
+ #------------------------------------------------------------
294
+ def instruction_action
295
+ if copy?
296
+ "-codec:#{output_specifier} copy \\"
297
+ elsif transcode?
298
+ if codec_type == 'audio'
299
+ encode_to = @defaults[:codecs_audio_preferred][0]
300
+ elsif codec_type == 'video'
301
+ encode_to = @defaults[:codecs_video_preferred][0]
302
+ else
303
+ raise Exception.new "Error: somehow the program branched where it shouldn't have."
304
+ end
305
+ "-codec:#{output_specifier} #{encoder_string(for_codec: encode_to)} \\"
306
+ else
307
+ nil
308
+ end
309
+ end
310
+
311
+ #------------------------------------------------------------
312
+ # Property - returns an instruction for setting the metadata
313
+ # of the stream, if necessary.
314
+ #------------------------------------------------------------
315
+ def instruction_metadata
316
+ # We only want to set fixed_lang if options allow us to fix the language,
317
+ # and we want to set subtitle language from the filename, if applicable.
318
+ fixed_lang = @defaults[:fix_undefined_language] ? @defaults[:undefined_language] : nil
319
+ lang = subtitle_file_language ? subtitle_file_language : fixed_lang
320
+ set_language = set_language? ? "language=#{lang} " : nil
321
+ set_title = title && ! @defaults[:ignore_titles] ? "title=\"#{title}\" " : nil
322
+
323
+ if set_language || set_title
324
+ "-metadata:s:#{output_specifier} #{set_language}#{set_title}\\"
325
+ else
326
+ nil
327
+ end
328
+ end
329
+
330
+ #------------------------------------------------------------
331
+ # Property - returns an instruction for setting the stream's
332
+ # default disposition, if necessary.
333
+ #------------------------------------------------------------
334
+ def instruction_disposition
335
+ set_disposition = output_unique? && !default? && !drop? ? "default " : nil
336
+
337
+ if set_disposition
338
+ "-disposition:#{output_specifier} #{set_disposition}\\"
339
+ else
340
+ nil
341
+ end
342
+ end
343
+
344
+
345
+ #============================================================
346
+ private
347
+ #============================================================
348
+
349
+
350
+ #------------------------------------------------------------
351
+ # Property - returns an array of actions that are suggested
352
+ # for the stream based on quality, language, codec, etc.
353
+ #------------------------------------------------------------
354
+ def actions
355
+
356
+ #------------------------------------------------------------
357
+ # Note: logic below a result of Karnaugh mapping of the
358
+ # selection truth table for each desired action. There's
359
+ # probably an excel file somewhere in the repository.
360
+ #------------------------------------------------------------
361
+
362
+ if @actions.nil?
363
+ @actions = []
364
+
365
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
366
+ # subtitle stream handler
367
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
368
+ if codec_type == 'subtitle'
369
+
370
+ a = @defaults[:keep_langs_subs]&.include?(language)
371
+ b = @defaults[:codecs_subs_preferred]&.include?(codec_name)
372
+ c = language.downcase == 'und'
373
+ d = title != nil && ! @defaults[:ignore_titles]
374
+
375
+ if (!a && !c) || (!b)
376
+ @actions |= [:drop]
377
+ else
378
+ @actions |= [:copy]
379
+ end
380
+
381
+ if (b && c) && (@defaults[:fix_undefined_language])
382
+ @actions |= [:set_language]
383
+ end
384
+
385
+ if (!a || !b || c || (d))
386
+ @actions |= [:interesting]
387
+ end
388
+
389
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
390
+ # video stream handler
391
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
392
+ elsif codec_type == 'video'
393
+
394
+ a = codec_name.downcase == 'mjpeg'
395
+ b = @defaults[:codecs_video_preferred]&.include?(codec_name)
396
+ c = @defaults[:keep_langs_video]&.include?(language)
397
+ d = language.downcase == 'und'
398
+ e = title != nil && ! @defaults[:ignore_titles]
399
+ f = @defaults[:scan_type] == 'quality' && low_quality?
400
+
401
+ if (a)
402
+ @actions |= [:drop]
403
+ end
404
+
405
+ if (!a && b)
406
+ @actions |= [:copy]
407
+ end
408
+
409
+ if (!a && !b)
410
+ @actions |= [:transcode]
411
+ end
412
+
413
+ if (!a && d) && (@defaults[:fix_undefined_language])
414
+ @actions |= [:set_language]
415
+ end
416
+
417
+ if (a || !b || !c || d || e || f)
418
+ @actions |= [:interesting]
419
+ end
420
+
421
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
422
+ # audio stream handler
423
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
424
+ elsif codec_type == 'audio'
425
+
426
+ a = @defaults[:codecs_audio_preferred]&.include?(codec_name)
427
+ b = @defaults[:keep_langs_audio]&.include?(language)
428
+ c = language.downcase == 'und'
429
+ d = title != nil && ! @defaults[:ignore_titles]
430
+ e = @defaults[:scan_type] == 'quality' && low_quality?
431
+
432
+ if (!b && !c)
433
+ @actions |= one_of_a_kind? ? [:set_language] : [:drop]
434
+ end
435
+
436
+ if (a && b) || (a && !b && c)
437
+ @actions |= [:copy]
438
+ end
439
+
440
+ if (!a && !b && c) || (!a && b)
441
+ @actions |= [:transcode]
442
+ end
443
+
444
+ if (c) && (@defaults[:fix_undefined_language])
445
+ @actions |= [:set_language]
446
+ end
447
+
448
+ if (!a || !b || c || d || e)
449
+ @actions |= [:interesting]
450
+ end
451
+
452
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
453
+ # other stream handler
454
+ #––––––––––––––––––––––––––––––––––––––––––––––––––
455
+ else
456
+ @actions |= [:drop]
457
+ end
458
+ end # if @actions.nil?
459
+
460
+ @actions
461
+ end # actions
462
+
463
+ #------------------------------------------------------------
464
+ # Given a codec, return the ffmpeg encoder string.
465
+ #------------------------------------------------------------
466
+ def encoder_string(for_codec:)
467
+ case for_codec.downcase
468
+ when 'hevc'
469
+ "libx265 -crf 28 -preset slow"
470
+ when 'h264'
471
+ "libx264 -crf 23 -preset slow"
472
+ when 'aac'
473
+ "libfdk_aac"
474
+ else
475
+ raise Exception.new "Error: somehow an unsupported codec '#{for_codec}' was specified."
476
+ end
477
+ end
478
+
479
+ #------------------------------------------------------------
480
+ # If the source file is an srt, and there's a language, and
481
+ # it's in the approved language list, then return it;
482
+ # otherwise return nil.
483
+ #------------------------------------------------------------
484
+ def subtitle_file_language
485
+ langs = @defaults[:keep_langs_subs]&.join('|')
486
+ lang = @source_file.match(/^.*\.(#{langs})\.srt$/)
487
+ lang ? lang[1] : nil
488
+ end
489
+
490
+ end # class
491
+
492
+ end # module