Fri, 24 Nov 2017 23:11:58 +0100
added option parser and automatic file rename feature
7 | 1 | #!/usr/bin/env python |
2 | ||
3 | import subprocess | |
9 | 4 | from eit import readeit, eitinfo |
7 | 5 | import os, shlex |
6 | ||
7 | def filter_lines(data, search): | |
8 | ret = [] | |
9 | for line in data.split("\n"): | |
10 | if line.find(search) == -1: | |
11 | continue | |
12 | ret.append(line) | |
13 | return "\n".join(ret) | |
14 | ||
15 | def run_command(command): | |
16 | process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) | |
17 | while True: | |
18 | output = process.stdout.readline() | |
19 | if output == '' and process.poll() is not None: | |
20 | break | |
21 | if output: | |
22 | print output.strip() | |
23 | rc = process.poll() | |
24 | return rc | |
25 | ||
26 | class ts2mkv(object): | |
9 | 27 | def __init__(self, crf=19, tune='film', scaleto_720p=True, rename=False): |
7 | 28 | self.msg_prepare = "" |
29 | self.msg_eit = "" | |
30 | self.msg_ffmpeg = "" | |
31 | self.command = None | |
32 | ||
9 | 33 | self.scaleto_720p = scaleto_720p |
34 | self.rename = rename | |
8 | 35 | |
7 | 36 | self.video_options = [ |
37 | "-c:v libx264", | |
8 | 38 | "-preset faster", # slow |
9 | 39 | "-tune %s" % tune, # film / animation |
40 | "-crf %i" % crf # 21, better 19 | |
7 | 41 | ] |
42 | self.audio_options = [ | |
43 | "-c:a copy", | |
44 | ] | |
45 | ||
46 | def get_stream_index(self, data): | |
47 | idx = data.find("Stream #") | |
48 | if idx == -1: | |
49 | return "" | |
50 | idx += 8 | |
51 | self.msg_prepare += "Selecting: %s\n" % data | |
52 | return data[idx:idx+3] | |
53 | ||
9 | 54 | def get_movie_description(self): |
55 | if not self.filename: | |
56 | return | |
7 | 57 | # read the EIT file |
58 | # TODO: fallback to meta file if no EIT | |
59 | # TODO: is there a way to get the imdb for the movie automagically? | |
60 | # http://www.omdbapi.com/apikey.aspx | |
9 | 61 | filename = os.path.splitext(self.filename)[0] + ".eit" |
62 | self.msg_eit = readeit(filename) | |
63 | if not self.rename or not self.msg_eit: | |
64 | return | |
65 | info = eitinfo(filename) | |
66 | name = info.eit.get("name") | |
67 | if name == "": | |
68 | # cancel rename, no movie title found! | |
69 | return | |
70 | genre = info.eit.get("genre") | |
71 | if genre != "": | |
72 | name = "%s (%s)" % (name, genre) | |
73 | # build new filename | |
74 | name = name.replace(' : ', ' - ') | |
75 | name = name.replace(': ', ' - ') | |
76 | name = name.replace(':', '-') | |
77 | name = name.replace('/', '') | |
78 | name = name.replace('\\', '') | |
79 | name = name.replace('?', '') | |
80 | name = name.replace('*', '') | |
81 | name = name.replace('\"', '\'') | |
82 | ||
83 | self.outfilebase = os.path.join( | |
84 | os.path.dirname(filename), | |
85 | name | |
86 | ) | |
87 | ||
7 | 88 | |
8 | 89 | def get_crop_option(self): |
90 | lines = filter_lines(self.msg_ffmpeg, "[Parsed_cropdetect").split("\n") | |
91 | option = None | |
92 | for line in lines: | |
93 | tmp = line[line.find(" crop="):].strip() | |
94 | #print "DEBUG: " + tmp | |
95 | if not option: | |
96 | option = tmp | |
97 | else: | |
98 | if option != tmp: | |
99 | self.msg_prepare += "WARNING: cropdetect inconsistent over scan time, disabling autocrop\n" | |
100 | return None | |
101 | self.msg_prepare += "Crop detected: %s\n" % option | |
102 | return option | |
103 | ||
9 | 104 | def get_ffmpeg_command(self): |
105 | if not self.filename: | |
106 | return None | |
107 | ||
7 | 108 | commands = [] |
9 | 109 | fn = "\\'".join(p for p in self.filename.split("'")) |
110 | fn = fn.replace(" ", "\\ ") | |
111 | outfn = self.outfilebase + ".mkv" | |
112 | outfn = "\\'".join(p for p in outfn.split("'")) | |
113 | outfn = outfn.replace(" ", "\\ ") | |
7 | 114 | |
8 | 115 | # ffmpeg -ss 00:05:00 -t 2 -i testfiles/chappie.ts -vf "cropdetect=24:16:0" -f null - |
116 | ||
117 | cmd = ["ffmpeg", | |
118 | "-ss 00:05:00", "-t 1", # search to 5 minutes, analyze 1 second | |
119 | "-i %s" % fn, | |
120 | "-vf \"cropdetect=24:2:0\"", # detect black bar crop on top and bottom | |
121 | "-f null", "-" # no output file | |
122 | ] | |
123 | print " ".join(cmd) | |
124 | p = subprocess.Popen(shlex.split(" ".join(cmd)), stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
7 | 125 | out, err = p.communicate() |
8 | 126 | print "Command return code: ", p.poll() |
7 | 127 | self.msg_ffmpeg = out + "\n" + err |
128 | self.msg_ffmpeg = self.msg_ffmpeg[self.msg_ffmpeg.find("Input #0"):] | |
8 | 129 | |
7 | 130 | # find "Stream #0:" lines |
131 | info = filter_lines(self.msg_ffmpeg, "Stream #0:") | |
132 | ||
133 | ||
134 | # CRAP! ffmpeg cannot decode dvb_teletext streams to srt :( | |
135 | # are there any subtitle streams?! | |
136 | #s = filter_lines(info, "Subtitle:") | |
137 | #s = filter_lines(s, "(deu):") | |
138 | #if s != "": | |
139 | # s = self.get_stream_index(s.split("\n")[0]) | |
140 | # commands.append([ | |
141 | # "ffmpeg", | |
142 | # "-txt_format text", | |
143 | # "-i %s" % fn, | |
144 | # "-map %s" % s, | |
145 | # "%s.srt" % os.path.splitext(fn)[0] | |
146 | # ]) | |
147 | ||
148 | v = self.get_stream_index( | |
149 | filter_lines(info, "Video:")) | |
150 | if v == "": | |
151 | print "No video stream found" | |
152 | return None | |
153 | ||
9 | 154 | audiomap = [] |
7 | 155 | audioall = filter_lines(info, "Audio:") |
156 | audio = filter_lines(audioall, "(deu):") | |
157 | a = self.get_stream_index( | |
158 | filter_lines(audio, "ac3")) | |
159 | # TODO: wenn kein ac3 stream dann dts oder mpeg fallback | |
160 | if a == "": | |
9 | 161 | print audioall |
162 | print "No AC3 german audio stream found" | |
163 | # try to find the first german audio stream | |
164 | a = self.get_stream_index(audio.split("\n")[0]) | |
165 | if a == "": | |
166 | print "No other german audio streams, trying english ac3..." | |
167 | else: | |
168 | print "Selecting first german stream." | |
169 | audiomap.append(a) | |
170 | else: | |
171 | audiomap.append(a) | |
7 | 172 | |
173 | audio = filter_lines(audioall, "(eng):") | |
174 | a = self.get_stream_index( | |
175 | filter_lines(audio, "ac3")) | |
176 | if a != "": | |
177 | # append english audio too! | |
9 | 178 | print "Selecting english ac3 stream." |
7 | 179 | audiomap.append(a) |
180 | ||
9 | 181 | if len(audiomap) == 0: |
182 | print "No suitable audio stream found, aborting." | |
183 | return None | |
7 | 184 | |
8 | 185 | |
7 | 186 | self.msg_prepare += "Video Stream selected: Stream #%s\n" % v |
187 | cmd = [ | |
188 | "ffmpeg", | |
189 | "-i %s" % fn, | |
190 | "-map %s" % v, | |
191 | ] | |
8 | 192 | flt = [] |
193 | crop = self.get_crop_option() | |
194 | if crop: | |
195 | flt.append(crop) | |
196 | if self.scaleto_720p: | |
197 | flt.append("scale='min(1280,iw)':-2'") # -2 ensures division by two for codec | |
198 | self.msg_prepare += "Scaling cropped output stream to 720p\n" | |
199 | if len(flt) > 0: | |
200 | # append video filters | |
201 | cmd.append('-filter:v "%s"' % ",".join(flt)) | |
7 | 202 | for a in audiomap: |
203 | self.msg_prepare += "Audio Stream selected: Stream #%s\n" % a | |
204 | cmd.append("-map %s" % a) | |
205 | cmd.extend(self.video_options) | |
206 | cmd.extend(self.audio_options) | |
9 | 207 | cmd.append(outfn) |
7 | 208 | |
209 | commands.append(" ".join(cmd)) | |
210 | return commands | |
211 | ||
212 | def load(self, filename): | |
213 | self.filename = filename | |
9 | 214 | self.outfilebase = os.path.splitext(filename)[0] |
215 | self.get_movie_description() | |
216 | self.command = self.get_ffmpeg_command() | |
7 | 217 | |
9 | 218 | def convert(self): |
219 | if not self.command: | |
220 | return None | |
221 | fd = open(self.outfilebase + ".txt", "wb") | |
222 | fd.write(self.msg_eit) | |
7 | 223 | fd.write("\n\n# ---DEBUG---\n\n") |
9 | 224 | fd.write(self.msg_prepare) |
225 | fd.write(self.msg_ffmpeg) | |
7 | 226 | fd.close() |
9 | 227 | #print self.msg_ffmpeg |
7 | 228 | |
229 | for cmd in mkv.command: | |
8 | 230 | print "Executing ffmpeg:\n%s\n" % cmd |
9 | 231 | return run_command(cmd) |
232 | ||
233 | ||
234 | ||
235 | if __name__ == "__main__": | |
236 | # parse command line options | |
237 | import argparse, sys, glob | |
238 | ||
239 | parser = argparse.ArgumentParser(description = 'DVB-TS to MKV kung-fu') | |
240 | parser.add_argument('--crf', type=int, default=19, | |
241 | help='h264 crf (default 19)') | |
242 | parser.add_argument('--tune', default='film', | |
243 | help='ffmpeg tune preset [film, animation] (default is film)') | |
244 | parser.add_argument('--ns', action='store_true', default=False, | |
245 | help='no rescaling (default is scale to 720p)') | |
246 | parser.add_argument('--rename', action='store_true', default=False, | |
247 | help='rename file basename to name and genre from EIT file if present') | |
248 | parser.add_argument('input', metavar='input', nargs='+', | |
249 | help='one or more files, glob style syntax') | |
250 | ||
251 | args = parser.parse_args() | |
252 | mkv = ts2mkv( | |
253 | crf = args.crf, | |
254 | tune = args.tune, | |
255 | scaleto_720p = (not args.ns), | |
256 | rename = args.rename | |
257 | ) | |
258 | ||
259 | #os.system('cls' if os.name == 'nt' else 'clear') | |
260 | ||
261 | for srcstr in args.input: | |
262 | src = glob.glob(srcstr) | |
263 | for filename in src: | |
264 | print filename | |
265 | mkv.load(filename) | |
266 | mkv.convert() | |
267 | ||
268 |