Thu, 04 Oct 2018 00:43:26 +0200
fix: cropdetect
7 | 1 | #!/usr/bin/env python |
10 | 2 | """ |
3 | DVB-TS to MKV kung-fu | |
4 | 2017 by mdd | |
5 | ||
6 | Toolkit / executable to automagically convert DVB recordings to h264 mkv. | |
17 | 7 | Automatic audio stream selection |
8 | deu: ac3, otherwise fallback to first german stream | |
9 | eng: ac3, no fallback | |
10 | 10 | Automatic crop detection to remove cinematic bars |
17 | 11 | percentage + ETA for ffmpeg conversion subprocess |
10 | 12 | """ |
17 | 13 | #pylint: disable=line-too-long |
14 | #pylint: disable=invalid-name | |
15 | ||
7 | 16 | |
17 | import subprocess | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
18 | import pexpect |
17 | 19 | from eit import eitinfo |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
20 | import os, shlex, sys, time |
7 | 21 | |
22 | def filter_lines(data, search): | |
10 | 23 | """ |
24 | input: data = \n separated string | |
17 | 25 | output: all lines where search is found |
10 | 26 | """ |
7 | 27 | ret = [] |
28 | for line in data.split("\n"): | |
29 | if line.find(search) == -1: | |
30 | continue | |
31 | ret.append(line) | |
32 | return "\n".join(ret) | |
33 | ||
34 | def run_command(command): | |
10 | 35 | """ |
36 | run command as blocking subprocess, returns exit code | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
37 | if total_frames > 0 parse ffmpeg status line and insert ETA at line start before output |
10 | 38 | """ |
39 | process = subprocess.Popen(shlex.split(command), \ | |
40 | stdout=subprocess.PIPE) | |
7 | 41 | while True: |
42 | output = process.stdout.readline() | |
43 | if output == '' and process.poll() is not None: | |
44 | break | |
45 | if output: | |
46 | print output.strip() | |
47 | rc = process.poll() | |
48 | return rc | |
49 | ||
17 | 50 | def run_ffmpeg_watch(command, frames_total=0): |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
51 | """ |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
52 | run command as blocking subprocess, returns exit code |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
53 | if total_frames > 0 parse ffmpeg status line and insert ETA at line start before output |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
54 | """ |
17 | 55 | #pylint: disable=maybe-no-member |
56 | ||
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
57 | thread = pexpect.spawn(command) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
58 | cpl = thread.compile_pattern_list([ |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
59 | pexpect.EOF, |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
60 | "frame= *(\d+)", |
16 | 61 | "(.+)\n", |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
62 | '(.+)' |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
63 | ]) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
64 | percent = 0 |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
65 | eta = 0 |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
66 | time_start = time.time() - 0.1 # start in the past |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
67 | while True: |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
68 | i = thread.expect_list(cpl, timeout=None) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
69 | if i == 0: # EOF |
16 | 70 | print "\nffmpeg subprocess finished!" |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
71 | break |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
72 | elif i == 1: |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
73 | try: |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
74 | frame_number = int(thread.match.group(1)) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
75 | if frames_total > 0: |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
76 | percent = frame_number * 100.00 / frames_total |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
77 | eta = frame_number / (time.time() - time_start) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
78 | # eta is frames per second so far |
24
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
79 | if eta == 0: |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
80 | eta = 1 |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
81 | eta = (frames_total - frame_number) / eta / 60 |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
82 | sys.stdout.write("\rFrame %i of %i, %.1f%% done, ETA %.0f minutes, " % ( |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
83 | frame_number, frames_total, percent, eta |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
84 | )) |
17 | 85 | except ValueError: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
86 | sys.stdout.write(thread.match.group(0)) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
87 | sys.stdout.flush() |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
88 | thread.close |
17 | 89 | #elif i == 2: |
16 | 90 | # normal newline line, just ignore them... |
17 | 91 | # pass |
16 | 92 | elif i == 3: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
93 | unknown_line = thread.match.group(0) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
94 | sys.stdout.write(unknown_line) |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
95 | sys.stdout.flush() |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
96 | thread.close() |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
97 | return thread.exitstatus |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
98 | |
14 | 99 | def ffmpeg_filename(filename): |
100 | """ | |
101 | Escape filename path contents for ffmpeg shell command | |
102 | """ | |
17 | 103 | fn = filename.replace("'", r"\'") |
104 | fn = fn.replace(" ", r"\ ") | |
14 | 105 | return fn |
106 | ||
7 | 107 | class ts2mkv(object): |
10 | 108 | """ |
109 | Main worker class, contains all the magic & ffmpeg voodoo | |
110 | """ | |
17 | 111 | def __init__(self, crf=19, tune='film'): |
7 | 112 | self.command = None |
13
cf5c5cec1b2b
bugfix: cleanup status messages when processing multiple files
mdd
parents:
12
diff
changeset
|
113 | self.filename = None |
cf5c5cec1b2b
bugfix: cleanup status messages when processing multiple files
mdd
parents:
12
diff
changeset
|
114 | self.outfilebase = None |
17 | 115 | self.info = {} |
116 | self.__reset() | |
8 | 117 | |
17 | 118 | self.config = { |
119 | "overwrite": False, | |
120 | "scaledown": True, | |
121 | "rename": True, | |
122 | "video": [ | |
123 | "-c:v libx264", | |
124 | "-preset faster", # slow | |
125 | "-tune %s" % tune, # film / animation | |
126 | "-crf %i" % crf, # 21, better 19 | |
127 | ], | |
128 | "audio": [ | |
129 | "-c:a copy", | |
130 | ] | |
131 | } | |
10 | 132 | |
17 | 133 | def __reset(self): |
134 | """ | |
135 | Reset internal stuff before loading new task | |
136 | """ | |
137 | self.info = { | |
138 | "msg_prepare": "", | |
139 | "msg_eit": "", | |
140 | "msg_ffmpeg": "", | |
141 | "fps": 0, | |
142 | "frames_total": 0 | |
143 | } | |
144 | self.command = None | |
145 | self.filename = None | |
146 | self.outfilebase = None | |
7 | 147 | |
148 | def get_stream_index(self, data): | |
10 | 149 | """ |
150 | input: ffmpeg stream info string | |
151 | output: ffmpeg stream mapping part | |
152 | """ | |
7 | 153 | idx = data.find("Stream #") |
154 | if idx == -1: | |
155 | return "" | |
156 | idx += 8 | |
24
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
157 | self.info["msg_prepare"] += "GetStreamIndex: %s\n" % data.strip() |
7 | 158 | return data[idx:idx+3] |
159 | ||
17 | 160 | def __get_movie_description(self): |
10 | 161 | """ |
162 | looks for eit file with same basename of current filename | |
163 | parse the eit file for txt infofile and optional build new | |
164 | output filename base with movie name and genre | |
165 | ||
166 | output: nothing, manipulates internal variables | |
167 | """ | |
9 | 168 | if not self.filename: |
169 | return | |
7 | 170 | # read the EIT file |
9 | 171 | filename = os.path.splitext(self.filename)[0] + ".eit" |
17 | 172 | info = eitinfo(filename) |
173 | self.info["msg_eit"] = info.dump() | |
174 | if not self.config["rename"] or not self.info["msg_eit"]: | |
9 | 175 | return |
176 | name = info.eit.get("name") | |
177 | if name == "": | |
178 | # cancel rename, no movie title found! | |
179 | return | |
180 | genre = info.eit.get("genre") | |
181 | if genre != "": | |
182 | name = "%s (%s)" % (name, genre) | |
183 | # build new filename | |
184 | name = name.replace(' : ', ' - ') | |
185 | name = name.replace(': ', ' - ') | |
186 | name = name.replace(':', '-') | |
187 | name = name.replace('/', '') | |
188 | name = name.replace('\\', '') | |
189 | name = name.replace('?', '') | |
190 | name = name.replace('*', '') | |
191 | name = name.replace('\"', '\'') | |
192 | ||
193 | self.outfilebase = os.path.join( | |
194 | os.path.dirname(filename), | |
195 | name | |
196 | ) | |
197 | ||
7 | 198 | |
8 | 199 | def get_crop_option(self): |
10 | 200 | """ |
201 | parse the ffmpeg analyze output cropdetect lines | |
202 | returns None or valid crop string for ffmpeg video filter | |
203 | """ | |
34 | 204 | if not self.config["cropdetect"]: |
205 | return None | |
17 | 206 | lines = filter_lines(self.info["msg_ffmpeg"], "[Parsed_cropdetect").split("\n") |
8 | 207 | option = None |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
208 | failcount = 0 |
8 | 209 | for line in lines: |
210 | tmp = line[line.find(" crop="):].strip() | |
25
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
211 | # crop=1920:804:0:138 |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
212 | if len(tmp.split(":")) != 4: |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
213 | print "Warning, invalid cropdetect: %s" % tmp |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
214 | return None |
34 | 215 | try: |
216 | if int(tmp.split(":")[2]) > 4: | |
217 | print "!!! X crop detected, disabling autocrop (%s)" % tmp | |
218 | self.info["msg_prepare"] += "WARNING: cropdetect suggested X crop, disabling autocrop\n" | |
219 | return None | |
220 | except ValueError: | |
221 | print "invalid crop x shift value, ignoring autocrop" | |
25
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
222 | return None |
8 | 223 | #print "DEBUG: " + tmp |
224 | if not option: | |
225 | option = tmp | |
226 | else: | |
227 | if option != tmp: | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
228 | failcount += 1 |
26 | 229 | if failcount > 6: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
230 | print "!!! Crop detect is inconsistent" |
26 | 231 | self.info["msg_prepare"] += "WARNING: cropdetect inconsistent, disabling autocrop\n" |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
232 | return None |
17 | 233 | self.info["msg_prepare"] += "Crop detected: %s\n" % option |
8 | 234 | return option |
235 | ||
14 | 236 | def __get_audiomap(self, info): |
237 | """ | |
238 | Select the wanted german and english audio streams from ffmpeg info | |
239 | output: mapping list | |
240 | """ | |
241 | audiomap = [] | |
242 | audioall = filter_lines(info, "Audio:") | |
243 | audio = filter_lines(audioall, "(deu):") | |
244 | aidx = self.get_stream_index( | |
245 | filter_lines(audio, "ac3")) | |
246 | if aidx == "": | |
247 | print audioall | |
248 | print "No AC3 german audio stream found" | |
249 | # try to find the first german audio stream | |
250 | aidx = self.get_stream_index(audio.split("\n")[0]) | |
251 | if aidx == "": | |
252 | print "No other german audio streams, trying english..." | |
253 | else: | |
254 | print "Selecting first german stream." | |
255 | audiomap.append(aidx) | |
256 | else: | |
257 | audiomap.append(aidx) | |
258 | ||
259 | audio = filter_lines(audioall, "(eng):") | |
260 | aidx = self.get_stream_index( | |
261 | filter_lines(audio, "ac3")) | |
24
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
262 | if aidx != "": |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
263 | try: |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
264 | filter_lines(audio, "ac3").index(" 0 channels") |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
265 | print "Skipping english stream with 0 channels" |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
266 | except ValueError: |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
267 | # append english audio too! |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
268 | print "Selecting english ac3 stream." |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
269 | audiomap.append(aidx) |
20 | 270 | if len(audiomap) == 0 and self.config["firstaudio"]: |
271 | # append first audio stream as forced fallback | |
272 | aidx = self.get_stream_index(audioall) | |
273 | if aidx != "": | |
274 | print "Forcing first found audio stream: %s" % aidx | |
275 | audiomap.append(aidx) | |
14 | 276 | return audiomap |
277 | ||
17 | 278 | def __parse_info(self): |
279 | """ | |
280 | get total duration and fps from input stream | |
281 | output: sets local variables | |
282 | # Duration: 01:39:59.88, start: 93674.825111, bitrate: 9365 kb/s | |
283 | # Stream #0:1[0x1ff]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709), 1920x1080 [SAR 1:1 DAR 16:9], 25 fps, 50 tbr, 90k tbn, 50 tbc | |
284 | """ | |
285 | tmp = filter_lines(self.info["msg_ffmpeg"], "Duration:").strip()[10:] | |
286 | tmp = tmp[0:tmp.find(",")].strip() | |
287 | print "Input duration: %s" % tmp | |
288 | try: | |
289 | self.info["frames_total"] = int(tmp[0:2]) * 3600 + \ | |
290 | int(tmp[3:5]) * 60 + int(tmp[6:8]) | |
291 | except ValueError: | |
292 | self.info["frames_total"] = 0 | |
293 | ||
294 | tmp = filter_lines(self.info["msg_ffmpeg"], "Stream #0:") | |
295 | tmp = filter_lines(tmp, "Video:").split(",") | |
296 | for fps in tmp: | |
297 | if fps.strip().endswith('fps'): | |
298 | try: | |
299 | self.info["fps"] = float(fps.strip().split(' ')[0]) | |
300 | except ValueError: | |
301 | self.info["fps"] = 0 | |
302 | break | |
303 | self.info["frames_total"] = round(self.info["frames_total"] * self.info["fps"], 0) | |
304 | print "Input framerate: %f fps" % self.info["fps"] | |
305 | print "Total frames of input file: %i" % (self.info["frames_total"]) | |
306 | ||
307 | ||
26 | 308 | def __get_ffmpeg_input_info(self, filename, crop_minute = 5): |
17 | 309 | """ |
310 | Run ffmpeg for cropdetect and general input information | |
311 | """ | |
312 | cmd = [ | |
313 | "ffmpeg", "-hide_banner", | |
26 | 314 | "-ss 00:%02i:00" % crop_minute, "-t 1", # search to 5 minutes, analyze 1 seconds |
17 | 315 | "-i %s" % filename, |
316 | "-vf \"cropdetect=24:2:0\"", # detect black bar crop on top and bottom | |
317 | "-f null", "-" # no output file | |
318 | ] | |
319 | p = subprocess.Popen(shlex.split(" ".join(cmd)), \ | |
320 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
321 | out, err = p.communicate() | |
18 | 322 | self.info["msg_ffmpeg"] = out + "\n" + err |
323 | self.info["msg_ffmpeg"] = self.info["msg_ffmpeg"][self.info["msg_ffmpeg"].find("Input #0"):] | |
17 | 324 | |
9 | 325 | def get_ffmpeg_command(self): |
10 | 326 | """ |
327 | Too complex to describe, this does all the magic | |
328 | output: produces internal ffmpeg command list (empty command list on error) | |
329 | """ | |
9 | 330 | if not self.filename: |
331 | return None | |
332 | ||
17 | 333 | fn = { |
334 | "in": ffmpeg_filename(self.filename), | |
335 | "out": self.outfilebase + ".mkv" | |
336 | } | |
11 | 337 | |
338 | # double-check: pull the kill switch and exit if outfile exists already! | |
339 | # we do not want to overwrite files in accident (caused by automatic file naming) | |
17 | 340 | if not self.config["overwrite"] and len(glob.glob(fn["out"])) > 0: |
341 | print "Output file exists: %s" % fn["out"] | |
11 | 342 | print "NOT overwriting it!" |
343 | return None | |
7 | 344 | |
17 | 345 | # load input file to get informations about |
346 | self.__get_ffmpeg_input_info(fn["in"]) | |
8 | 347 | |
7 | 348 | # find "Stream #0:" lines |
17 | 349 | info = filter_lines(self.info["msg_ffmpeg"], "Stream #0:") |
7 | 350 | |
351 | v = self.get_stream_index( | |
352 | filter_lines(info, "Video:")) | |
353 | if v == "": | |
354 | print "No video stream found" | |
355 | return None | |
356 | ||
17 | 357 | self.__parse_info() |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
358 | |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
359 | # copy ALL subtitle streams if present! |
14 | 360 | # Stream #0:0[0x20](deu): Subtitle: dvb_teletext ([6][0][0][0] / 0x0006), 492x250 |
361 | submap = [] | |
362 | for tmp in filter_lines(info, "Subtitle: dvb_teletext").split("\n"): | |
363 | if self.get_stream_index(tmp): | |
364 | submap.append(self.get_stream_index(tmp)) | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
365 | # Subtitles disabled, that doesnt work as expected, dreambox crashes on copied subtitle stream |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
366 | submap = [] |
7 | 367 | |
14 | 368 | # select audio streams |
369 | audiomap = self.__get_audiomap(info) | |
9 | 370 | if len(audiomap) == 0: |
371 | print "No suitable audio stream found, aborting." | |
372 | return None | |
7 | 373 | |
14 | 374 | # Old dreambox images did a file split: .ts .ts.001 .ts.002 etc. |
11 | 375 | # Find all these files and join them! |
17 | 376 | inputs = [fn["in"]] |
377 | if os.path.splitext(self.filename)[1].lower() == '.ts': | |
378 | for tmp in glob.glob(self.filename + '.' + ('[0-9]' * 3)): | |
379 | inputs.append(ffmpeg_filename(tmp)) | |
12 | 380 | |
381 | if len(inputs) > 1: | |
382 | # use ffmpeg input concat function | |
17 | 383 | # attention, ffmpeg concat protocol doesnt like escape sequences |
19 | 384 | for tmp in range(len(inputs)): |
385 | inputs[tmp] = inputs[tmp].replace(r"\ ", " ").replace(r"\'", "'")\ | |
386 | ||
387 | fn["in"] = "\"concat:" + "|".join(inputs) + "\"" | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
388 | # no ETA calculation possible since we have only the length of first file |
19 | 389 | # we could estimate by multiplying with factor generated by input file sizes |
390 | totalbytes = 0.0 | |
391 | for tmp in inputs: | |
392 | totalbytes += os.path.getsize(tmp) | |
393 | print "estimating total frames for ETA based on file sizes (we have multiple inputs here)" | |
394 | self.info["frames_total"] *= totalbytes / os.path.getsize(inputs[0]) | |
12 | 395 | |
396 | idx = 0 | |
397 | for tmp in inputs: | |
17 | 398 | self.info["msg_prepare"] += "Input file #%i: %s\n" % ( |
12 | 399 | idx, os.path.basename(tmp)) |
400 | idx += 1 | |
11 | 401 | |
7 | 402 | cmd = [ |
14 | 403 | "ffmpeg", "-hide_banner", |
17 | 404 | "-i %s" % fn["in"], |
7 | 405 | ] |
14 | 406 | |
17 | 407 | if self.config["overwrite"]: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
408 | cmd.append("-y") |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
409 | |
14 | 410 | for tmp in submap: |
17 | 411 | self.info["msg_prepare"] += "Subtitle Stream selected: Stream #%s\n" % tmp |
14 | 412 | cmd.append("-map %s" % tmp) |
413 | ||
414 | cmd.append("-map %s" % v) | |
17 | 415 | self.info["msg_prepare"] += "Video Stream selected: Stream #%s\n" % v |
14 | 416 | |
8 | 417 | flt = [] |
418 | crop = self.get_crop_option() | |
26 | 419 | if not crop: |
420 | # load input file to get informations about | |
421 | # scan to other position and try again | |
422 | print "Scanning again for autocrop..." | |
423 | self.info["msg_prepare"] += "Rescan autocrop on other position in input stream...\n" | |
424 | self.__get_ffmpeg_input_info(fn["in"], 9) | |
425 | crop = self.get_crop_option() | |
426 | ||
8 | 427 | if crop: |
428 | flt.append(crop) | |
17 | 429 | if self.config["scaledown"]: |
10 | 430 | # -2 ensures division by two for codec |
431 | flt.append("scale='min(1280,iw)':-2'") | |
17 | 432 | self.info["msg_prepare"] += "Scaling output stream to 720p if width >1280\n" |
8 | 433 | if len(flt) > 0: |
434 | # append video filters | |
435 | cmd.append('-filter:v "%s"' % ",".join(flt)) | |
17 | 436 | |
14 | 437 | for tmp in audiomap: |
17 | 438 | self.info["msg_prepare"] += "Audio Stream selected: Stream #%s\n" % tmp |
14 | 439 | cmd.append("-map %s" % tmp) |
440 | if len(submap) > 0: | |
441 | cmd.append("-c:s dvdsub") | |
17 | 442 | cmd.extend(self.config["video"]) |
443 | cmd.extend(self.config["audio"]) | |
444 | cmd.append(ffmpeg_filename(fn["out"])) | |
7 | 445 | |
17 | 446 | return [" ".join(cmd)] |
7 | 447 | |
448 | def load(self, filename): | |
10 | 449 | """ |
450 | First step: setup, analyze & prepare for conversion | |
451 | """ | |
17 | 452 | self.__reset() |
13
cf5c5cec1b2b
bugfix: cleanup status messages when processing multiple files
mdd
parents:
12
diff
changeset
|
453 | |
7 | 454 | self.filename = filename |
9 | 455 | self.outfilebase = os.path.splitext(filename)[0] |
17 | 456 | self.__get_movie_description() |
9 | 457 | self.command = self.get_ffmpeg_command() |
7 | 458 | |
9 | 459 | def convert(self): |
10 | 460 | """ |
461 | Second step: write info text file and start ffmpeg conversion | |
462 | requires successful load as first step | |
463 | returns ffmpeg conversion exit status | |
464 | """ | |
9 | 465 | if not self.command: |
466 | return None | |
20 | 467 | if not self.info["msg_eit"]: |
468 | self.info["msg_eit"] = "No EIT file found, sorry - no description" | |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
469 | if not self.config["dryrun"]: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
470 | fd = open(self.outfilebase + ".txt", "wb") |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
471 | fd.write(self.info["msg_eit"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
472 | fd.write("\n\n# ---DEBUG---\n\n") |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
473 | fd.write(self.info["msg_prepare"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
474 | fd.write(self.info["msg_ffmpeg"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
475 | fd.close() |
17 | 476 | #print self.info["msg_ffmpeg"] |
7 | 477 | |
10 | 478 | for cmd in self.command: |
8 | 479 | print "Executing ffmpeg:\n%s\n" % cmd |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
480 | #return run_command(cmd, self.total_frames) |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
481 | if not self.config["dryrun"]: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
482 | return run_ffmpeg_watch(cmd, frames_total=self.info["frames_total"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
483 | else: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
484 | return 0 |
9 | 485 | |
486 | ||
487 | ||
488 | if __name__ == "__main__": | |
489 | # parse command line options | |
10 | 490 | import argparse, glob |
9 | 491 | |
10 | 492 | parser = argparse.ArgumentParser(description='DVB-TS to MKV kung-fu') |
493 | parser.add_argument('--crf', type=int, default=19, \ | |
9 | 494 | help='h264 crf (default 19)') |
10 | 495 | parser.add_argument('--tune', default='film', \ |
9 | 496 | help='ffmpeg tune preset [film, animation] (default is film)') |
10 | 497 | parser.add_argument('--ns', action='store_true', default=False, \ |
9 | 498 | help='no rescaling (default is scale to 720p)') |
34 | 499 | parser.add_argument('--nc', action='store_true', default=False, \ |
500 | help='no crop detection') | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
501 | parser.add_argument('-f', action='store_true', default=False, \ |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
502 | help='force overwrite of existing file') |
20 | 503 | parser.add_argument('--fa', action='store_true', default=False, \ |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
504 | help='force use first audio stream') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
505 | parser.add_argument('--rename', action='store_true', default=False, \ |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
506 | help='rename file basename to name and genre from EIT file if present') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
507 | parser.add_argument('--moveto', default='', \ |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
508 | help='specify base directory to move processed files to') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
509 | parser.add_argument('--dryrun', action='store_true', default=False, \ |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
510 | help='Dry-run, dont touch anything') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
511 | parser.add_argument('input', metavar='input', nargs='+', \ |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
512 | help='one or more files, glob style syntax') |
9 | 513 | |
514 | args = parser.parse_args() | |
17 | 515 | processor = ts2mkv(crf=args.crf, tune=args.tune) |
18 | 516 | processor.config["scaledown"] = not args.ns |
34 | 517 | processor.config["cropdetect"] = not args.nc |
18 | 518 | processor.config["rename"] = args.rename |
519 | processor.config["overwrite"] = args.f | |
20 | 520 | processor.config["firstaudio"] = args.fa |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
521 | processor.config["dryrun"] = args.dryrun |
9 | 522 | |
26 | 523 | src = [] |
9 | 524 | for srcstr in args.input: |
26 | 525 | src.extend(glob.glob(srcstr)) |
526 | idx = 1 | |
527 | for srcfile in src: | |
528 | print "\nProcessing file %i/%i: %s" % (idx, len(src), srcfile) | |
529 | processor.load(srcfile) | |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
530 | exitcode = processor.convert() |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
531 | if exitcode == 0: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
532 | print "Successful conversion." |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
533 | if args.moveto: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
534 | mvlist = glob.glob(os.path.splitext(srcfile)[0] + ".*") |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
535 | mvtarget = os.path.dirname(srcfile).replace('../', '') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
536 | mvtarget = os.path.join( |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
537 | args.moveto, mvtarget) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
538 | mvsource = os.path.dirname(srcfile) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
539 | print "Moving processed files from %s to %s" % ( |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
540 | mvsource, mvtarget) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
541 | if not args.dryrun: |
30 | 542 | try: |
543 | os.makedirs(mvtarget) | |
544 | except OSError, e: | |
545 | if e.errno != os.errno.EEXIST: | |
546 | raise | |
547 | pass | |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
548 | for mvsrc in mvlist: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
549 | mvfn = os.path.basename(mvsrc) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
550 | if os.path.splitext(mvfn)[1] in ['.txt', '.mkv', '.nfo']: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
551 | continue |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
552 | print mvfn |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
553 | if not args.dryrun: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
554 | os.rename( |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
555 | os.path.join(mvsource, mvfn), |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
556 | os.path.join(mvtarget, mvfn)) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
557 | else: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
558 | print "ERROR while executing ffmpeg!" |
26 | 559 | idx += 1 |
9 | 560 | |
561 |