Tue, 12 Dec 2017 03:25:44 +0100
added stats calc to dupechecker
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 | """ | |
17 | 204 | lines = filter_lines(self.info["msg_ffmpeg"], "[Parsed_cropdetect").split("\n") |
8 | 205 | option = None |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
206 | failcount = 0 |
8 | 207 | for line in lines: |
208 | tmp = line[line.find(" crop="):].strip() | |
25
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
209 | # crop=1920:804:0:138 |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
210 | if len(tmp.split(":")) != 4: |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
211 | print "Warning, invalid cropdetect: %s" % tmp |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
212 | return None |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
213 | if tmp.split(":")[2] != "0": |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
214 | print "!!! X crop detected, disabling autocrop (%s)" % tmp |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
215 | self.info["msg_prepare"] += "WARNING: cropdetect suggested X crop, disabling autocrop\n" |
078802773343
ignore bad cropdetect, cancel crop when detection suggests X cropping
mdd
parents:
24
diff
changeset
|
216 | return None |
8 | 217 | #print "DEBUG: " + tmp |
218 | if not option: | |
219 | option = tmp | |
220 | else: | |
221 | if option != tmp: | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
222 | failcount += 1 |
26 | 223 | if failcount > 6: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
224 | print "!!! Crop detect is inconsistent" |
26 | 225 | 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
|
226 | return None |
17 | 227 | self.info["msg_prepare"] += "Crop detected: %s\n" % option |
8 | 228 | return option |
229 | ||
14 | 230 | def __get_audiomap(self, info): |
231 | """ | |
232 | Select the wanted german and english audio streams from ffmpeg info | |
233 | output: mapping list | |
234 | """ | |
235 | audiomap = [] | |
236 | audioall = filter_lines(info, "Audio:") | |
237 | audio = filter_lines(audioall, "(deu):") | |
238 | aidx = self.get_stream_index( | |
239 | filter_lines(audio, "ac3")) | |
240 | if aidx == "": | |
241 | print audioall | |
242 | print "No AC3 german audio stream found" | |
243 | # try to find the first german audio stream | |
244 | aidx = self.get_stream_index(audio.split("\n")[0]) | |
245 | if aidx == "": | |
246 | print "No other german audio streams, trying english..." | |
247 | else: | |
248 | print "Selecting first german stream." | |
249 | audiomap.append(aidx) | |
250 | else: | |
251 | audiomap.append(aidx) | |
252 | ||
253 | audio = filter_lines(audioall, "(eng):") | |
254 | aidx = self.get_stream_index( | |
255 | filter_lines(audio, "ac3")) | |
24
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
256 | if aidx != "": |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
257 | try: |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
258 | 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
|
259 | 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
|
260 | except ValueError: |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
261 | # append english audio too! |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
262 | print "Selecting english ac3 stream." |
a1bc75496992
div0 bug if we have frame errors at the video stream beginning
mdd
parents:
20
diff
changeset
|
263 | audiomap.append(aidx) |
20 | 264 | if len(audiomap) == 0 and self.config["firstaudio"]: |
265 | # append first audio stream as forced fallback | |
266 | aidx = self.get_stream_index(audioall) | |
267 | if aidx != "": | |
268 | print "Forcing first found audio stream: %s" % aidx | |
269 | audiomap.append(aidx) | |
14 | 270 | return audiomap |
271 | ||
17 | 272 | def __parse_info(self): |
273 | """ | |
274 | get total duration and fps from input stream | |
275 | output: sets local variables | |
276 | # Duration: 01:39:59.88, start: 93674.825111, bitrate: 9365 kb/s | |
277 | # 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 | |
278 | """ | |
279 | tmp = filter_lines(self.info["msg_ffmpeg"], "Duration:").strip()[10:] | |
280 | tmp = tmp[0:tmp.find(",")].strip() | |
281 | print "Input duration: %s" % tmp | |
282 | try: | |
283 | self.info["frames_total"] = int(tmp[0:2]) * 3600 + \ | |
284 | int(tmp[3:5]) * 60 + int(tmp[6:8]) | |
285 | except ValueError: | |
286 | self.info["frames_total"] = 0 | |
287 | ||
288 | tmp = filter_lines(self.info["msg_ffmpeg"], "Stream #0:") | |
289 | tmp = filter_lines(tmp, "Video:").split(",") | |
290 | for fps in tmp: | |
291 | if fps.strip().endswith('fps'): | |
292 | try: | |
293 | self.info["fps"] = float(fps.strip().split(' ')[0]) | |
294 | except ValueError: | |
295 | self.info["fps"] = 0 | |
296 | break | |
297 | self.info["frames_total"] = round(self.info["frames_total"] * self.info["fps"], 0) | |
298 | print "Input framerate: %f fps" % self.info["fps"] | |
299 | print "Total frames of input file: %i" % (self.info["frames_total"]) | |
300 | ||
301 | ||
26 | 302 | def __get_ffmpeg_input_info(self, filename, crop_minute = 5): |
17 | 303 | """ |
304 | Run ffmpeg for cropdetect and general input information | |
305 | """ | |
306 | cmd = [ | |
307 | "ffmpeg", "-hide_banner", | |
26 | 308 | "-ss 00:%02i:00" % crop_minute, "-t 1", # search to 5 minutes, analyze 1 seconds |
17 | 309 | "-i %s" % filename, |
310 | "-vf \"cropdetect=24:2:0\"", # detect black bar crop on top and bottom | |
311 | "-f null", "-" # no output file | |
312 | ] | |
313 | p = subprocess.Popen(shlex.split(" ".join(cmd)), \ | |
314 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
315 | out, err = p.communicate() | |
18 | 316 | self.info["msg_ffmpeg"] = out + "\n" + err |
317 | self.info["msg_ffmpeg"] = self.info["msg_ffmpeg"][self.info["msg_ffmpeg"].find("Input #0"):] | |
17 | 318 | |
9 | 319 | def get_ffmpeg_command(self): |
10 | 320 | """ |
321 | Too complex to describe, this does all the magic | |
322 | output: produces internal ffmpeg command list (empty command list on error) | |
323 | """ | |
9 | 324 | if not self.filename: |
325 | return None | |
326 | ||
17 | 327 | fn = { |
328 | "in": ffmpeg_filename(self.filename), | |
329 | "out": self.outfilebase + ".mkv" | |
330 | } | |
11 | 331 | |
332 | # double-check: pull the kill switch and exit if outfile exists already! | |
333 | # we do not want to overwrite files in accident (caused by automatic file naming) | |
17 | 334 | if not self.config["overwrite"] and len(glob.glob(fn["out"])) > 0: |
335 | print "Output file exists: %s" % fn["out"] | |
11 | 336 | print "NOT overwriting it!" |
337 | return None | |
7 | 338 | |
17 | 339 | # load input file to get informations about |
340 | self.__get_ffmpeg_input_info(fn["in"]) | |
8 | 341 | |
7 | 342 | # find "Stream #0:" lines |
17 | 343 | info = filter_lines(self.info["msg_ffmpeg"], "Stream #0:") |
7 | 344 | |
345 | v = self.get_stream_index( | |
346 | filter_lines(info, "Video:")) | |
347 | if v == "": | |
348 | print "No video stream found" | |
349 | return None | |
350 | ||
17 | 351 | self.__parse_info() |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
352 | |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
353 | # copy ALL subtitle streams if present! |
14 | 354 | # Stream #0:0[0x20](deu): Subtitle: dvb_teletext ([6][0][0][0] / 0x0006), 492x250 |
355 | submap = [] | |
356 | for tmp in filter_lines(info, "Subtitle: dvb_teletext").split("\n"): | |
357 | if self.get_stream_index(tmp): | |
358 | 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
|
359 | # 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
|
360 | submap = [] |
7 | 361 | |
14 | 362 | # select audio streams |
363 | audiomap = self.__get_audiomap(info) | |
9 | 364 | if len(audiomap) == 0: |
365 | print "No suitable audio stream found, aborting." | |
366 | return None | |
7 | 367 | |
14 | 368 | # Old dreambox images did a file split: .ts .ts.001 .ts.002 etc. |
11 | 369 | # Find all these files and join them! |
17 | 370 | inputs = [fn["in"]] |
371 | if os.path.splitext(self.filename)[1].lower() == '.ts': | |
372 | for tmp in glob.glob(self.filename + '.' + ('[0-9]' * 3)): | |
373 | inputs.append(ffmpeg_filename(tmp)) | |
12 | 374 | |
375 | if len(inputs) > 1: | |
376 | # use ffmpeg input concat function | |
17 | 377 | # attention, ffmpeg concat protocol doesnt like escape sequences |
19 | 378 | for tmp in range(len(inputs)): |
379 | inputs[tmp] = inputs[tmp].replace(r"\ ", " ").replace(r"\'", "'")\ | |
380 | ||
381 | fn["in"] = "\"concat:" + "|".join(inputs) + "\"" | |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
382 | # no ETA calculation possible since we have only the length of first file |
19 | 383 | # we could estimate by multiplying with factor generated by input file sizes |
384 | totalbytes = 0.0 | |
385 | for tmp in inputs: | |
386 | totalbytes += os.path.getsize(tmp) | |
387 | print "estimating total frames for ETA based on file sizes (we have multiple inputs here)" | |
388 | self.info["frames_total"] *= totalbytes / os.path.getsize(inputs[0]) | |
12 | 389 | |
390 | idx = 0 | |
391 | for tmp in inputs: | |
17 | 392 | self.info["msg_prepare"] += "Input file #%i: %s\n" % ( |
12 | 393 | idx, os.path.basename(tmp)) |
394 | idx += 1 | |
11 | 395 | |
7 | 396 | cmd = [ |
14 | 397 | "ffmpeg", "-hide_banner", |
17 | 398 | "-i %s" % fn["in"], |
7 | 399 | ] |
14 | 400 | |
17 | 401 | if self.config["overwrite"]: |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
402 | cmd.append("-y") |
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
403 | |
14 | 404 | for tmp in submap: |
17 | 405 | self.info["msg_prepare"] += "Subtitle Stream selected: Stream #%s\n" % tmp |
14 | 406 | cmd.append("-map %s" % tmp) |
407 | ||
408 | cmd.append("-map %s" % v) | |
17 | 409 | self.info["msg_prepare"] += "Video Stream selected: Stream #%s\n" % v |
14 | 410 | |
8 | 411 | flt = [] |
412 | crop = self.get_crop_option() | |
26 | 413 | if not crop: |
414 | # load input file to get informations about | |
415 | # scan to other position and try again | |
416 | print "Scanning again for autocrop..." | |
417 | self.info["msg_prepare"] += "Rescan autocrop on other position in input stream...\n" | |
418 | self.__get_ffmpeg_input_info(fn["in"], 9) | |
419 | crop = self.get_crop_option() | |
420 | ||
8 | 421 | if crop: |
422 | flt.append(crop) | |
17 | 423 | if self.config["scaledown"]: |
10 | 424 | # -2 ensures division by two for codec |
425 | flt.append("scale='min(1280,iw)':-2'") | |
17 | 426 | self.info["msg_prepare"] += "Scaling output stream to 720p if width >1280\n" |
8 | 427 | if len(flt) > 0: |
428 | # append video filters | |
429 | cmd.append('-filter:v "%s"' % ",".join(flt)) | |
17 | 430 | |
14 | 431 | for tmp in audiomap: |
17 | 432 | self.info["msg_prepare"] += "Audio Stream selected: Stream #%s\n" % tmp |
14 | 433 | cmd.append("-map %s" % tmp) |
434 | if len(submap) > 0: | |
435 | cmd.append("-c:s dvdsub") | |
17 | 436 | cmd.extend(self.config["video"]) |
437 | cmd.extend(self.config["audio"]) | |
438 | cmd.append(ffmpeg_filename(fn["out"])) | |
7 | 439 | |
17 | 440 | return [" ".join(cmd)] |
7 | 441 | |
442 | def load(self, filename): | |
10 | 443 | """ |
444 | First step: setup, analyze & prepare for conversion | |
445 | """ | |
17 | 446 | self.__reset() |
13
cf5c5cec1b2b
bugfix: cleanup status messages when processing multiple files
mdd
parents:
12
diff
changeset
|
447 | |
7 | 448 | self.filename = filename |
9 | 449 | self.outfilebase = os.path.splitext(filename)[0] |
17 | 450 | self.__get_movie_description() |
9 | 451 | self.command = self.get_ffmpeg_command() |
7 | 452 | |
9 | 453 | def convert(self): |
10 | 454 | """ |
455 | Second step: write info text file and start ffmpeg conversion | |
456 | requires successful load as first step | |
457 | returns ffmpeg conversion exit status | |
458 | """ | |
9 | 459 | if not self.command: |
460 | return None | |
20 | 461 | if not self.info["msg_eit"]: |
462 | 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
|
463 | if not self.config["dryrun"]: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
464 | fd = open(self.outfilebase + ".txt", "wb") |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
465 | fd.write(self.info["msg_eit"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
466 | 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
|
467 | fd.write(self.info["msg_prepare"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
468 | fd.write(self.info["msg_ffmpeg"]) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
469 | fd.close() |
17 | 470 | #print self.info["msg_ffmpeg"] |
7 | 471 | |
10 | 472 | for cmd in self.command: |
8 | 473 | 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
|
474 | #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
|
475 | if not self.config["dryrun"]: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
476 | 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
|
477 | else: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
478 | return 0 |
9 | 479 | |
480 | ||
481 | ||
482 | if __name__ == "__main__": | |
483 | # parse command line options | |
10 | 484 | import argparse, glob |
9 | 485 | |
10 | 486 | parser = argparse.ArgumentParser(description='DVB-TS to MKV kung-fu') |
487 | parser.add_argument('--crf', type=int, default=19, \ | |
9 | 488 | help='h264 crf (default 19)') |
10 | 489 | parser.add_argument('--tune', default='film', \ |
9 | 490 | help='ffmpeg tune preset [film, animation] (default is film)') |
10 | 491 | parser.add_argument('--ns', action='store_true', default=False, \ |
9 | 492 | help='no rescaling (default is scale to 720p)') |
15
82361ad7b3fe
some changes, also implemented ffmpeg progress info and added force overwrite mode
mdd
parents:
14
diff
changeset
|
493 | 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
|
494 | help='force overwrite of existing file') |
20 | 495 | 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
|
496 | help='force use first audio stream') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
497 | 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
|
498 | 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
|
499 | parser.add_argument('--moveto', default='', \ |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
500 | 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
|
501 | 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
|
502 | help='Dry-run, dont touch anything') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
503 | 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
|
504 | help='one or more files, glob style syntax') |
9 | 505 | |
506 | args = parser.parse_args() | |
17 | 507 | processor = ts2mkv(crf=args.crf, tune=args.tune) |
18 | 508 | processor.config["scaledown"] = not args.ns |
509 | processor.config["rename"] = args.rename | |
510 | processor.config["overwrite"] = args.f | |
20 | 511 | processor.config["firstaudio"] = args.fa |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
512 | processor.config["dryrun"] = args.dryrun |
9 | 513 | |
26 | 514 | src = [] |
9 | 515 | for srcstr in args.input: |
26 | 516 | src.extend(glob.glob(srcstr)) |
517 | idx = 1 | |
518 | for srcfile in src: | |
519 | print "\nProcessing file %i/%i: %s" % (idx, len(src), srcfile) | |
520 | processor.load(srcfile) | |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
521 | exitcode = processor.convert() |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
522 | if exitcode == 0: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
523 | print "Successful conversion." |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
524 | if args.moveto: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
525 | 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
|
526 | mvtarget = os.path.dirname(srcfile).replace('../', '') |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
527 | mvtarget = os.path.join( |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
528 | args.moveto, mvtarget) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
529 | mvsource = os.path.dirname(srcfile) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
530 | 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
|
531 | mvsource, mvtarget) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
532 | if not args.dryrun: |
30 | 533 | try: |
534 | os.makedirs(mvtarget) | |
535 | except OSError, e: | |
536 | if e.errno != os.errno.EEXIST: | |
537 | raise | |
538 | pass | |
27
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
539 | for mvsrc in mvlist: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
540 | mvfn = os.path.basename(mvsrc) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
541 | 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
|
542 | continue |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
543 | print mvfn |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
544 | if not args.dryrun: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
545 | os.rename( |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
546 | os.path.join(mvsource, mvfn), |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
547 | os.path.join(mvtarget, mvfn)) |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
548 | else: |
a2951f7c435e
added dryrun for testing, option to move processed files to another location
mdd
parents:
26
diff
changeset
|
549 | print "ERROR while executing ffmpeg!" |
26 | 550 | idx += 1 |
9 | 551 | |
552 |