Commit | Line | Data |
---|---|---|
04a05f7f MW |
1 | #! /usr/bin/python3 |
2 | ### -*- mode: python; coding: utf-8 -*- | |
3 | ||
4 | from contextlib import contextmanager | |
0becf74e | 5 | import errno as E |
151b3c4f | 6 | import optparse as OP |
04a05f7f MW |
7 | import os as OS |
8 | import re as RX | |
0becf74e | 9 | import sqlite3 as SQL |
1bec83d0 | 10 | import subprocess as SP |
04a05f7f MW |
11 | import sys as SYS |
12 | ||
13 | class ExpectedError (Exception): pass | |
14 | ||
15 | @contextmanager | |
16 | def location(loc): | |
17 | global LOC | |
18 | old, LOC = LOC, loc | |
19 | yield loc | |
20 | LOC = old | |
21 | ||
22 | def filter(value, func = None, dflt = None): | |
23 | if value is None: return dflt | |
24 | elif func is None: return value | |
25 | else: return func(value) | |
26 | ||
27 | def check(cond, msg): | |
28 | if not cond: raise ExpectedError(msg) | |
29 | ||
151b3c4f MW |
30 | def lookup(dict, key, msg): |
31 | try: return dict[key] | |
32 | except KeyError: raise ExpectedError(msg) | |
33 | ||
34 | def forget(dict, key): | |
35 | try: del dict[key] | |
36 | except KeyError: pass | |
37 | ||
04a05f7f MW |
38 | def getint(s): |
39 | if not s.isdigit(): raise ExpectedError("bad integer `%s'" % s) | |
40 | return int(s) | |
41 | ||
fb430389 MW |
42 | def getbool(s): |
43 | if s == "t": return True | |
44 | elif s == "nil": return False | |
45 | else: raise ExpectedError("bad boolean `%s'" % s) | |
46 | ||
1766dcfb MW |
47 | def quote(s): |
48 | if s is None: return "-" | |
49 | else: return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' | |
50 | ||
04a05f7f MW |
51 | class Words (object): |
52 | def __init__(me, s): | |
53 | me._s = s | |
54 | me._i, me._n = 0, len(s) | |
55 | def _wordstart(me): | |
56 | s, i, n = me._s, me._i, me._n | |
57 | while i < n: | |
58 | if not s[i].isspace(): return i | |
59 | i += 1 | |
60 | return -1 | |
61 | def nextword(me): | |
62 | s, n = me._s, me._n | |
63 | begin = i = me._wordstart() | |
64 | if begin < 0: return None | |
65 | while i < n and not s[i].isspace(): i += 1 | |
66 | me._i = i | |
67 | return s[begin:i] | |
68 | def rest(me): | |
69 | s, n = me._s, me._n | |
70 | begin = me._wordstart() | |
71 | if begin < 0: return None | |
72 | else: return s[begin:].rstrip() | |
73 | ||
1bec83d0 MW |
74 | def program_output(*args, **kw): |
75 | try: return SP.check_output(*args, **kw) | |
76 | except SP.CalledProcessError as e: | |
77 | raise ExpectedError("program `%s' failed with code %d" % | |
78 | (e.cmd, e.returncode)) | |
79 | ||
04a05f7f MW |
80 | URL_SAFE_P = 256*[False] |
81 | for ch in \ | |
82 | b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ | |
83 | b"abcdefghijklmnopqrstuvwxyz" \ | |
685ddde0 | 84 | b"0123456789" b"!$%_-.,/": |
04a05f7f MW |
85 | URL_SAFE_P[ch] = True |
86 | def urlencode(s): | |
87 | return "".join((URL_SAFE_P[ch] and chr(ch) or "%%%02x" % ch | |
88 | for ch in s.encode("UTF-8"))) | |
89 | ||
90 | PROG = OS.path.basename(SYS.argv[0]) | |
91 | ||
92 | class BaseLocation (object): | |
93 | def report(me, exc): | |
94 | SYS.stderr.write("%s: %s%s\n" % (PROG, me._loc(), exc)) | |
95 | ||
96 | class DummyLocation (BaseLocation): | |
97 | def _loc(me): return "" | |
98 | ||
99 | class FileLocation (BaseLocation): | |
100 | def __init__(me, fn, lno = 1): me._fn, me._lno = fn, lno | |
101 | def _loc(me): return "%s:%d: " % (me._fn, me._lno) | |
102 | def stepline(me): me._lno += 1 | |
103 | ||
104 | LOC = DummyLocation() | |
105 | ||
0becf74e MW |
106 | ROOT = "/mnt/dvd/archive/" |
107 | DB = None | |
108 | ||
109 | def init_db(fn): | |
110 | global DB | |
111 | DB = SQL.connect(fn) | |
112 | DB.cursor().execute("PRAGMA journal_mode = WAL") | |
113 | ||
114 | def setup_db(fn): | |
115 | try: OS.unlink(fn) | |
116 | except OSError as e: | |
117 | if e.errno == E.ENOENT: pass | |
118 | else: raise | |
119 | init_db(fn) | |
120 | DB.cursor().execute(""" | |
121 | CREATE TABLE duration | |
122 | (path TEXT NOT NULL, | |
123 | title INTEGER NOT NULL, | |
124 | start_chapter INTEGER NOT NULL, | |
125 | end_chapter INTEGER NOT NULL, | |
126 | inode INTEGER NOT NULL, | |
127 | device INTEGER NOT NULL, | |
128 | size INTEGER NOT NULL, | |
129 | mtime REAL NOT NULL, | |
130 | duration REAL NOT NULL, | |
131 | PRIMARY KEY (path, title, start_chapter, end_chapter)); | |
132 | """) | |
133 | ||
04a05f7f | 134 | class Source (object): |
3b9c615b | 135 | |
04a05f7f MW |
136 | PREFIX = "" |
137 | TITLEP = CHAPTERP = False | |
3b9c615b | 138 | |
04a05f7f MW |
139 | def __init__(me, fn): |
140 | me.fn = fn | |
b092d511 | 141 | me.neps = None |
4ae03d6c | 142 | me.used_titles = set() |
b092d511 MW |
143 | me.used_chapters = set() |
144 | me.nuses = 0 | |
3b9c615b | 145 | |
1bec83d0 MW |
146 | def _duration(me, title, start_chapter, end_chapter): |
147 | return -1 | |
d4f60571 MW |
148 | |
149 | def url_and_duration(me, title = -1, start_chapter = -1, end_chapter = -1): | |
150 | if title == -1: | |
04a05f7f | 151 | if me.TITLEP: raise ExpectedError("missing title number") |
d4f60571 | 152 | if start_chapter != -1 or end_chapter != -1: |
04a05f7f MW |
153 | raise ExpectedError("can't specify chapter without title") |
154 | suffix = "" | |
155 | elif not me.TITLEP: | |
156 | raise ExpectedError("can't specify title with `%s'" % me.fn) | |
d4f60571 MW |
157 | elif start_chapter == -1: |
158 | if end_chapter != -1: | |
fd3b422f | 159 | raise ExpectedError("can't specify end chapter without start chapter") |
04a05f7f MW |
160 | suffix = "#%d" % title |
161 | elif not me.CHAPTERP: | |
162 | raise ExpectedError("can't specify chapter with `%s'" % me.fn) | |
d4f60571 | 163 | elif end_chapter == -1: |
fd3b422f | 164 | suffix = "#%d:%d" % (title, start_chapter) |
04a05f7f | 165 | else: |
fd3b422f | 166 | suffix = "#%d:%d-%d:%d" % (title, start_chapter, title, end_chapter - 1) |
1bec83d0 | 167 | |
0becf74e MW |
168 | duration = None |
169 | if DB is None: | |
170 | duration = me._duration(title, start_chapter, end_chapter) | |
171 | else: | |
172 | st = OS.stat(OS.path.join(ROOT, me.fn)) | |
173 | duration = None | |
174 | c = DB.cursor() | |
175 | c.execute(""" | |
176 | SELECT device, inode, size, mtime, duration FROM duration | |
177 | WHERE path = ? AND title = ? AND | |
178 | start_chapter = ? AND end_chapter = ? | |
d4f60571 | 179 | """, [me.fn, title, start_chapter, end_chapter]) |
0becf74e MW |
180 | row = c.fetchone() |
181 | foundp = False | |
182 | if row is None: | |
183 | duration = me._duration(title, start_chapter, end_chapter) | |
184 | c.execute(""" | |
185 | INSERT OR REPLACE INTO duration | |
186 | (path, title, start_chapter, end_chapter, | |
187 | device, inode, size, mtime, duration) | |
188 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
d4f60571 | 189 | """, [me.fn, title, start_chapter, end_chapter, |
0becf74e MW |
190 | st.st_dev, st.st_ino, st.st_size, st.st_mtime, |
191 | duration]) | |
192 | else: | |
193 | dev, ino, sz, mt, d = row | |
194 | if (dev, ino, sz, mt) == \ | |
195 | (st.st_dev, st.st_ino, st.st_size, st.st_mtime): | |
196 | duration = d | |
197 | else: | |
198 | duration = me._duration(title, start_chapter, end_chapter) | |
199 | c.execute(""" | |
200 | UPDATE duration | |
201 | SET device = ?, inode = ?, size = ?, mtime = ?, duration = ? | |
202 | WHERE path = ? AND title = ? AND | |
203 | start_chapter = ? AND end_chapter = ? | |
d4f60571 MW |
204 | """, [st.st_dev, st.st_dev, st.st_size, st.st_mtime, duration, |
205 | me.fn, title, start_chapter, end_chapter]) | |
0becf74e | 206 | DB.commit() |
1bec83d0 | 207 | |
d4f60571 | 208 | if end_chapter != -1: |
fd3b422f MW |
209 | keys = [(title, ch) for ch in range(start_chapter, end_chapter)] |
210 | set = me.used_chapters | |
211 | else: | |
212 | keys, set = [title], me.used_titles | |
213 | for k in keys: | |
214 | if k in set: | |
d4f60571 | 215 | if title == -1: |
fd3b422f | 216 | raise ExpectedError("`%s' already used" % me.fn) |
d4f60571 | 217 | elif end_chapter == -1: |
fd3b422f MW |
218 | raise ExpectedError("`%s' title %d already used" % (me.fn, title)) |
219 | else: | |
220 | raise ExpectedError("`%s' title %d chapter %d already used" % | |
221 | (me.fn, title, k[1])) | |
4ae03d6c MW |
222 | if end_chapter == -1: |
223 | me.used_titles.add(title) | |
224 | else: | |
fd3b422f MW |
225 | for ch in range(start_chapter, end_chapter): |
226 | me.used_chapters.add((title, ch)) | |
1bec83d0 | 227 | return me.PREFIX + ROOT + urlencode(me.fn) + suffix, duration |
04a05f7f | 228 | |
7427f306 | 229 | class DVDFile (Source): |
04a05f7f MW |
230 | PREFIX = "dvd://" |
231 | TITLEP = CHAPTERP = True | |
232 | ||
b092d511 MW |
233 | def __init__(me, fn, *args, **kw): |
234 | super().__init__(fn, *args, **kw) | |
235 | me.neps = 0 | |
236 | ||
1bec83d0 MW |
237 | def _duration(me, title, start_chapter, end_chapter): |
238 | path = OS.path.join(ROOT, me.fn) | |
239 | ntitle = int(program_output(["dvd-info", path, "titles"])) | |
240 | if not 1 <= title <= ntitle: | |
241 | raise ExpectedError("bad title %d for `%s': must be in 1 .. %d" % | |
242 | (title, me.fn, ntitle)) | |
d4f60571 | 243 | if start_chapter == -1: |
1bec83d0 MW |
244 | durq = "duration:%d" % title |
245 | else: | |
246 | nch = int(program_output(["dvd-info", path, "chapters:%d" % title])) | |
d4f60571 | 247 | if end_chapter == -1: end_chapter = nch |
1bec83d0 MW |
248 | else: end_chapter -= 1 |
249 | if not 1 <= start_chapter <= end_chapter <= nch: | |
250 | raise ExpectedError("bad chapter range %d .. %d for `%s' title %d: " | |
251 | "must be in 1 .. %d" % | |
252 | (start_chapter, end_chapter, me.fn, title, nch)) | |
253 | durq = "duration:%d.%d-%d" % (title, start_chapter, end_chapter) | |
254 | duration = int(program_output(["dvd-info", path, durq])) | |
255 | return duration | |
256 | ||
7427f306 | 257 | class DVDSeason (object): |
04a05f7f MW |
258 | def __init__(me, i, title): |
259 | me.i = i | |
260 | me.title = title | |
261 | me.episodes = {} | |
32cd109c MW |
262 | def set_episode_disc(me, i, disc): |
263 | if i in me.episodes: | |
264 | raise ExpectedError("season %d episode %d already taken" % (me.i, i)) | |
b092d511 | 265 | me.episodes[i] = disc; disc.neps += 1 |
04a05f7f | 266 | |
dcb1cc6c MW |
267 | def match_group(m, *groups, dflt = None, mustp = False): |
268 | for g in groups: | |
269 | try: s = m.group(g) | |
270 | except IndexError: continue | |
04a05f7f | 271 | if s is not None: return s |
dcb1cc6c MW |
272 | if mustp: raise ValueError("no match found") |
273 | else: return dflt | |
04a05f7f | 274 | |
7427f306 | 275 | class DVDDir (object): |
04a05f7f | 276 | |
4f8020f7 MW |
277 | _R_ISO_PRE = list(map(lambda pats: |
278 | list(map(lambda pat: | |
279 | RX.compile("^" + pat + r"\.iso$", RX.X), | |
280 | pats)), | |
281 | [[r""" S (?P<si> \d+) \. \ (?P<stitle> .*) — (?: D \d+ \. \ )? | |
282 | (?P<epex> .*) """, | |
283 | r""" S (?P<si> \d+) (?: D \d+)? \. \ (?P<epex> .*) """, | |
284 | r""" S (?P<si> \d+) \. \ (?P<epex> E \d+ .*) """, | |
285 | r""" S (?P<si> \d+) (?P<epex> E \d+) \. \ .* """], | |
286 | [r""" (?P<si> \d+) [A-Z]? \. \ (?P<stitle> .*) — (?P<epex> .*) """], | |
287 | [r""" \d+ \. \ (?P<epex> [ES] \d+ .*) """], | |
288 | [r""" (?P<epnum> \d+ ) \. \ .* """]])) | |
04a05f7f | 289 | |
9fc467bb | 290 | _R_ISO_EP = RX.compile(r""" ^ |
6b5cec73 | 291 | (?: S (?P<si> \d+) \ )? |
9fc467bb | 292 | E (?P<ei> \d+) (?: – (?P<ej> \d+))? $ |
04a05f7f MW |
293 | """, RX.X) |
294 | ||
295 | def __init__(me, dir): | |
b092d511 | 296 | me.dir = dir |
04a05f7f MW |
297 | fns = OS.listdir(OS.path.join(ROOT, dir)) |
298 | fns.sort() | |
6b5cec73 | 299 | season = None |
04a05f7f | 300 | seasons = {} |
4f8020f7 | 301 | styles = me._R_ISO_PRE |
04a05f7f MW |
302 | for fn in fns: |
303 | path = OS.path.join(dir, fn) | |
304 | if not fn.endswith(".iso"): continue | |
dcb1cc6c | 305 | #print(";; `%s'" % path, file = SYS.stderr) |
4f8020f7 MW |
306 | for sty in styles: |
307 | for r in sty: | |
308 | m = r.match(fn) | |
309 | if m: styles = [sty]; break | |
310 | else: | |
311 | continue | |
312 | break | |
dcb1cc6c MW |
313 | else: |
314 | #print(";;\tignored (regex mismatch)", file = SYS.stderr) | |
065a5db6 | 315 | continue |
04a05f7f | 316 | |
dcb1cc6c MW |
317 | si = filter(match_group(m, "si"), int) |
318 | stitle = match_group(m, "stitle") | |
319 | ||
320 | check(si is not None or stitle is None, | |
6b5cec73 | 321 | "explicit season title without number in `%s'" % fn) |
dcb1cc6c MW |
322 | if si is not None: |
323 | if season is None or si != season.i: | |
324 | check(season is None or si == season.i + 1, | |
6b5cec73 | 325 | "season %d /= %d" % |
dcb1cc6c MW |
326 | (si, season is None and -1 or season.i + 1)) |
327 | check(si not in seasons, "season %d already seen" % si) | |
7427f306 | 328 | seasons[si] = season = DVDSeason(si, stitle) |
6b5cec73 MW |
329 | else: |
330 | check(stitle == season.title, | |
331 | "season title `%s' /= `%s'" % (stitle, season.title)) | |
04a05f7f | 332 | |
7427f306 | 333 | disc = DVDFile(path) |
6b5cec73 | 334 | ts = season |
32cd109c | 335 | any, bad = False, False |
dcb1cc6c MW |
336 | epnum = match_group(m, "epnum") |
337 | if epnum is not None: eplist = ["E" + epnum] | |
338 | else: eplist = match_group(m, "epex", mustp = True).split(", ") | |
35ecb6eb | 339 | for eprange in eplist: |
04a05f7f | 340 | mm = me._R_ISO_EP.match(eprange) |
3ee2c072 MW |
341 | if mm is None: |
342 | #print(";;\t`%s'?" % eprange, file = SYS.stderr) | |
343 | bad = True; continue | |
344 | if not any: any = True | |
6b5cec73 MW |
345 | i = filter(mm.group("si"), int) |
346 | if i is not None: | |
347 | try: ts = seasons[i] | |
7427f306 | 348 | except KeyError: ts = seasons[i] = DVDSeason(i, None) |
6b5cec73 | 349 | if ts is None: |
7427f306 | 350 | ts = season = seasons[1] = DVDSeason(1, None) |
04a05f7f MW |
351 | start = filter(mm.group("ei"), int) |
352 | end = filter(mm.group("ej"), int, start) | |
32cd109c | 353 | for k in range(start, end + 1): |
6b5cec73 | 354 | ts.set_episode_disc(k, disc) |
065a5db6 | 355 | #print(";;\tepisode %d.%d" % (ts.i, k), file = SYS.stderr) |
3ee2c072 | 356 | if not any: |
dcb1cc6c | 357 | #print(";;\tignored", file = SYS.stderr) |
3ee2c072 MW |
358 | pass |
359 | elif bad: | |
360 | raise ExpectedError("bad ep list in `%s'", fn) | |
04a05f7f MW |
361 | me.seasons = seasons |
362 | ||
72ddfb64 MW |
363 | class SingleFileDir (object): |
364 | ||
365 | _CHECK_COMPLETE = True | |
366 | ||
367 | def __init__(me, dir): | |
368 | me.dir = dir | |
369 | fns = OS.listdir(OS.path.join(ROOT, dir)) | |
370 | fns.sort() | |
371 | episodes = {} | |
372 | last_i = 0 | |
373 | rx = RX.compile(r""" | |
374 | E (\d+) | |
375 | (?: \. \ (.*))? | |
376 | %s $ | |
377 | """ % RX.escape(me._EXT), RX.X) | |
378 | ||
379 | for fn in fns: | |
380 | path = OS.path.join(dir, fn) | |
381 | if not fn.endswith(me._EXT): continue | |
382 | m = rx.match(fn) | |
383 | if not m: continue | |
384 | i = filter(m.group(1), int) | |
385 | etitle = m.group(2) | |
386 | if me._CHECK_COMPLETE: | |
387 | check(i == last_i + 1, "episode %d /= %d" % (i, last_i + 1)) | |
388 | episodes[i] = me._mkepisode(path, i) | |
389 | last_i = i | |
390 | me.episodes = episodes | |
391 | ||
7427f306 | 392 | class AudioFile (Source): |
fbac3340 | 393 | PREFIX = "file://" |
04a05f7f MW |
394 | TITLEP = CHAPTERP = False |
395 | ||
1bec83d0 MW |
396 | def _duration(me, title, start_chapter, end_chaptwr): |
397 | out = program_output(["metaflac", | |
398 | "--show-total-samples", "--show-sample-rate", | |
399 | OS.path.join(ROOT, me.fn)]) | |
400 | nsamples, hz = map(float, out.split()) | |
401 | return int(nsamples/hz) | |
402 | ||
7427f306 | 403 | class AudioEpisode (AudioFile): |
04a05f7f MW |
404 | def __init__(me, fn, i, *args, **kw): |
405 | super().__init__(fn, *args, **kw) | |
406 | me.i = i | |
407 | ||
72ddfb64 MW |
408 | class AudioDir (SingleFileDir): |
409 | _EXT = ".flac" | |
04a05f7f | 410 | |
72ddfb64 MW |
411 | def _mkepisode(me, path, i): |
412 | return AudioEpisode(path, i) | |
04a05f7f | 413 | |
eaa841b2 MW |
414 | class VideoFile (Source): |
415 | PREFIX = "file://" | |
416 | TITLEP = CHAPTERP = False | |
417 | ||
418 | def _duration(me, title, start_chapter, end_chaptwr): | |
419 | out = program_output(["mediainfo", "--output=General;%Duration%", | |
420 | OS.path.join(ROOT, me.fn)]) | |
421 | return int(out)//1000 | |
422 | ||
423 | class VideoEpisode (VideoFile): | |
424 | def __init__(me, fn, i, *args, **kw): | |
425 | super().__init__(fn, *args, **kw) | |
426 | me.i = i | |
427 | ||
428 | class VideoDir (SingleFileDir): | |
429 | _EXT = ".mp4" | |
430 | _CHECK_COMPLETE = False | |
431 | ||
432 | def _mkepisode(me, path, i): | |
433 | return VideoEpisode(path, i) | |
04a05f7f MW |
434 | |
435 | class Chapter (object): | |
436 | def __init__(me, episode, title, i): | |
437 | me.title, me.i = title, i | |
1bec83d0 MW |
438 | me.url, me.duration = \ |
439 | episode.source.url_and_duration(episode.tno, i, i + 1) | |
04a05f7f MW |
440 | |
441 | class Episode (object): | |
fb430389 | 442 | def __init__(me, season, i, neps, title, src, series_title_p = True, |
d4f60571 | 443 | tno = -1, startch = -1, endch = -1): |
04a05f7f MW |
444 | me.season = season |
445 | me.i, me.neps, me.title = i, neps, title | |
446 | me.chapters = [] | |
447 | me.source, me.tno = src, tno | |
fb430389 | 448 | me.series_title_p = series_title_p |
1766dcfb | 449 | me.tno, me.start_chapter, me.end_chapter = tno, startch, endch |
1bec83d0 | 450 | me.url, me.duration = src.url_and_duration(tno, startch, endch) |
04a05f7f MW |
451 | def add_chapter(me, title, j): |
452 | ch = Chapter(me, title, j) | |
453 | me.chapters.append(ch) | |
454 | return ch | |
455 | def label(me): | |
456 | return me.season._eplabel(me.i, me.neps, me.title) | |
457 | ||
151b3c4f MW |
458 | class BaseSeason (object): |
459 | def __init__(me, series, implicitp = False): | |
460 | me.series = series | |
04a05f7f | 461 | me.episodes = [] |
151b3c4f MW |
462 | me.implicitp = implicitp |
463 | me.ep_i, episodes = 1, [] | |
fb430389 MW |
464 | def add_episode(me, j, neps, title, src, series_title_p, |
465 | tno, startch, endch): | |
466 | ep = Episode(me, j, neps, title, src, series_title_p, | |
467 | tno, startch, endch) | |
04a05f7f | 468 | me.episodes.append(ep) |
151b3c4f | 469 | src.nuses += neps; me.ep_i += neps |
04a05f7f | 470 | return ep |
4a25b86c MW |
471 | def _epnames(me, i, neps): |
472 | playlist = me.series.playlist | |
c6b2a381 MW |
473 | if neps == 1: return playlist.epname, ["%d" % i] |
474 | elif neps == 2: return playlist.epnames, ["%d" % i, "%d" % (i + 1)] | |
475 | else: return playlist.epnames, ["%d–%d" % (i, i + neps - 1)] | |
151b3c4f MW |
476 | |
477 | class Season (BaseSeason): | |
478 | def __init__(me, series, title, i, *args, **kw): | |
479 | super().__init__(series, *args, **kw) | |
480 | me.title, me.i = title, i | |
04a05f7f | 481 | def _eplabel(me, i, neps, title): |
4a25b86c | 482 | epname, epn = me._epnames(i, neps) |
04a05f7f | 483 | if title is None: |
c6b2a381 MW |
484 | if me.implicitp: |
485 | label = "%s %s" % (epname, ", ".join(epn)) | |
486 | elif me.title is None: | |
487 | label = "%s %s" % \ | |
488 | (epname, ", ".join("%d.%s" % (me.i, e) for e in epn)) | |
489 | else: | |
b0c5ef2d | 490 | label = "%s: %s %s" % (me.title, epname, ", ".join(epn)) |
04a05f7f | 491 | else: |
c6b2a381 MW |
492 | if me.implicitp: |
493 | label = "%s. %s" % (", ".join(epn), title) | |
494 | elif me.title is None: | |
495 | label = "%s. %s" % \ | |
496 | (", ".join("%d.%s" % (me.i, e) for e in epn), title) | |
497 | else: | |
b0c5ef2d | 498 | label = "%s: %s. %s" % (me.title, ", ".join(epn), title) |
151b3c4f | 499 | return label |
04a05f7f | 500 | |
151b3c4f | 501 | class MovieSeason (BaseSeason): |
c3538df6 MW |
502 | def __init__(me, series, title, *args, **kw): |
503 | super().__init__(series, *args, **kw) | |
504 | me.title = title | |
5ca4c92e | 505 | me.i = None |
fb430389 MW |
506 | def add_episode(me, j, neps, title, src, series_title_p, |
507 | tno, startch, endch): | |
c3538df6 MW |
508 | if me.title is None and title is None: |
509 | raise ExpectedError("movie or movie season must have a title") | |
fb430389 MW |
510 | return super().add_episode(j, neps, title, src, series_title_p, |
511 | tno, startch, endch) | |
c3538df6 MW |
512 | def _eplabel(me, i, neps, title): |
513 | if me.title is None: | |
514 | label = title | |
515 | elif title is None: | |
516 | epname, epn = me._epnames(i, neps) | |
b0c5ef2d | 517 | label = "%s: %s %s" % (me.title, epname, ", ".join(epn)) |
c3538df6 | 518 | else: |
b0c5ef2d | 519 | label = "%s: %s" % (me.title, title) |
c3538df6 | 520 | return label |
04a05f7f | 521 | |
151b3c4f | 522 | class Series (object): |
2cf61248 MW |
523 | def __init__(me, playlist, name, title = None, |
524 | full_title = None, wantedp = True): | |
151b3c4f | 525 | me.playlist = playlist |
2cf61248 | 526 | me.name, me.title, me.full_title = name, title, full_title |
151b3c4f | 527 | me.cur_season = None |
08f08e7c | 528 | me.wantedp = wantedp |
151b3c4f MW |
529 | def _add_season(me, season): |
530 | me.cur_season = season | |
531 | def add_season(me, title, i, implicitp = False): | |
532 | me._add_season(Season(me, title, i, implicitp)) | |
c3538df6 MW |
533 | def add_movies(me, title = None): |
534 | me._add_season(MovieSeason(me, title)) | |
151b3c4f MW |
535 | def ensure_season(me): |
536 | if me.cur_season is None: me.add_season(None, 1, implicitp = True) | |
537 | return me.cur_season | |
538 | def end_season(me): | |
539 | me.cur_season = None | |
04a05f7f | 540 | |
151b3c4f | 541 | class Playlist (object): |
3b9c615b | 542 | |
04a05f7f MW |
543 | def __init__(me): |
544 | me.seasons = [] | |
151b3c4f | 545 | me.episodes = [] |
04a05f7f | 546 | me.epname, me.epnames = "Episode", "Episodes" |
151b3c4f | 547 | me.nseries = 0 |
d1f1c578 MW |
548 | me.single_series_p = False |
549 | me.series_title = None | |
f8b9c373 | 550 | me.series_sep = "" |
3b9c615b | 551 | |
151b3c4f MW |
552 | def add_episode(me, episode): |
553 | me.episodes.append(episode) | |
3b9c615b | 554 | |
151b3c4f MW |
555 | def done_season(me): |
556 | if me.episodes: | |
557 | me.seasons.append(me.episodes) | |
558 | me.episodes = [] | |
2f4f35b0 | 559 | |
04a05f7f MW |
560 | def write(me, f): |
561 | f.write("#EXTM3U\n") | |
562 | for season in me.seasons: | |
563 | f.write("\n") | |
151b3c4f MW |
564 | for ep in season: |
565 | label = ep.label() | |
fb430389 MW |
566 | if me.nseries > 1 and ep.series_title_p and \ |
567 | ep.season.series.title is not None: | |
f8b9c373 MW |
568 | if ep.season.i is None: sep = ":" |
569 | else: sep = me.series_sep | |
570 | label = ep.season.series.title + sep + " " + label | |
04a05f7f | 571 | if not ep.chapters: |
1bec83d0 | 572 | f.write("#EXTINF:%d,,%s\n%s\n" % (ep.duration, label, ep.url)) |
04a05f7f MW |
573 | else: |
574 | for ch in ep.chapters: | |
1bec83d0 | 575 | f.write("#EXTINF:%d,,%s: %s\n%s\n" % |
1766dcfb MW |
576 | (ch.duration, label, ch.title, ch.url)) |
577 | ||
578 | def dump(me, f): | |
e6650127 | 579 | if opts.list_name is not None: f.write("LIST %s\n" % opts.list_name) |
1766dcfb MW |
580 | if me.series_title is not None and \ |
581 | me.nseries > 1 and not me.single_series_p: | |
582 | raise ExpectedError("can't force series name for multi-series list") | |
be6d4012 | 583 | series = set() |
1766dcfb | 584 | if me.single_series_p: |
be6d4012 | 585 | f.write("SERIES - %s\n" % quote(me.series_title)) |
1766dcfb MW |
586 | for season in me.seasons: |
587 | for ep in season: | |
be6d4012 MW |
588 | label = ep.label() |
589 | title = ep.season.series.full_title | |
1766dcfb | 590 | if me.single_series_p: |
be6d4012 | 591 | stag = "-" |
f8b9c373 | 592 | if title is not None: label = title + me.series_sep + " " + label |
1766dcfb | 593 | else: |
be6d4012 MW |
594 | if title is None: title = me.series_title |
595 | stag = ep.season.series.name | |
596 | if stag is None: stag = "-" | |
597 | if stag not in series: | |
1766dcfb | 598 | f.write("SERIES %s %s\n" % (stag, quote(title))) |
be6d4012 MW |
599 | series.add(stag) |
600 | f.write("ENTRY %s %s %s %d %d %d %g\n" % | |
1766dcfb MW |
601 | (stag, quote(label), quote(ep.source.fn), |
602 | ep.tno, ep.start_chapter, ep.end_chapter, ep.duration)) | |
151b3c4f | 603 | |
2f4f35b0 MW |
604 | def write_deps(me, f, out): |
605 | deps = set() | |
606 | for season in me.seasons: | |
607 | for ep in season: deps.add(ep.source.fn) | |
608 | f.write("### -*-makefile-*-\n") | |
609 | f.write("%s: $(call check-deps, %s," % (out, out)) | |
610 | for dep in sorted(deps): | |
611 | f.write(" \\\n\t'%s'" % | |
612 | OS.path.join(ROOT, dep) | |
613 | .replace(",", "$(comma)") | |
614 | .replace("'", "'\\''")) | |
615 | f.write(")\n") | |
616 | ||
066e5d43 MW |
617 | DEFAULT_EXPVAR = 0.05 |
618 | R_DURMULT = RX.compile(r""" ^ | |
619 | (\d+ (?: \. \d+)?) x | |
620 | $ """, RX.X) | |
621 | R_DUR = RX.compile(r""" ^ | |
622 | (?: (?: (\d+) :)? (\d+) :)? (\d+) | |
623 | (?: / (\d+ (?: \. \d+)?) \%)? | |
624 | $ """, RX.X) | |
625 | def parse_duration(s, base = None, basevar = DEFAULT_EXPVAR): | |
626 | if base is not None: | |
627 | m = R_DURMULT.match(s) | |
628 | if m is not None: return base*float(m.group(1)), basevar | |
629 | m = R_DUR.match(s) | |
630 | if not m: raise ExpectedError("invalid duration spec `%s'" % s) | |
631 | hr, min, sec = map(lambda g: filter(m.group(g), int, 0), [1, 2, 3]) | |
632 | var = filter(m.group(4), lambda x: float(x)/100.0) | |
633 | if var is None: var = DEFAULT_EXPVAR | |
634 | return 3600*hr + 60*min + sec, var | |
635 | def format_duration(d): | |
636 | if d >= 3600: return "%d:%02d:%02d" % (d//3600, (d//60)%60, d%60) | |
637 | elif d >= 60: return "%d:%02d" % (d//60, d%60) | |
638 | else: return "%d s" % d | |
639 | ||
151b3c4f MW |
640 | MODE_UNSET = 0 |
641 | MODE_SINGLE = 1 | |
642 | MODE_MULTI = 2 | |
643 | ||
644 | class EpisodeListParser (object): | |
645 | ||
646 | def __init__(me, series_wanted = None, chapters_wanted_p = False): | |
647 | me._pl = Playlist() | |
648 | me._cur_episode = me._cur_chapter = None | |
7427f306 | 649 | me._series = {}; me._vdirs = {}; me._sfdirs = {}; me._isos = {} |
151b3c4f MW |
650 | me._series_wanted = series_wanted |
651 | me._chaptersp = chapters_wanted_p | |
066e5d43 | 652 | me._explen, me._expvar = None, DEFAULT_EXPVAR |
151b3c4f MW |
653 | if series_wanted is None: me._mode = MODE_UNSET |
654 | else: me._mode = MODE_MULTI | |
655 | ||
656 | def _bad_keyval(me, cmd, k, v): | |
657 | raise ExpectedError("invalid `!%s' option `%s'" % | |
658 | (cmd, v if k is None else k)) | |
659 | ||
660 | def _keyvals(me, opts): | |
661 | if opts is not None: | |
662 | for kv in opts.split(","): | |
663 | try: sep = kv.index("=") | |
664 | except ValueError: yield None, kv | |
665 | else: yield kv[:sep], kv[sep + 1:] | |
666 | ||
667 | def _set_mode(me, mode): | |
668 | if me._mode == MODE_UNSET: | |
669 | me._mode = mode | |
670 | elif me._mode != mode: | |
671 | raise ExpectedError("inconsistent single-/multi-series usage") | |
672 | ||
673 | def _get_series(me, name): | |
674 | if name is None: | |
675 | me._set_mode(MODE_SINGLE) | |
676 | try: series = me._series[None] | |
677 | except KeyError: | |
08f08e7c | 678 | series = me._series[None] = Series(me._pl, None) |
151b3c4f MW |
679 | me._pl.nseries += 1 |
680 | else: | |
681 | me._set_mode(MODE_MULTI) | |
682 | series = lookup(me._series, name, "unknown series `%s'" % name) | |
683 | return series | |
684 | ||
685 | def _opts_series(me, cmd, opts): | |
686 | name = None | |
687 | for k, v in me._keyvals(opts): | |
688 | if k is None: name = v | |
689 | else: me._bad_keyval(cmd, k, v) | |
08f08e7c | 690 | return me._get_series(name) |
151b3c4f | 691 | |
08f08e7c MW |
692 | def _auto_epsrc(me, series): |
693 | dir = lookup(me._vdirs, series.name, "no active video directory") | |
694 | season = series.ensure_season() | |
695 | check(season.i is not None, "must use explicit iso for movie seasons") | |
696 | vseason = lookup(dir.seasons, season.i, | |
697 | "season %d not found in video dir `%s'" % | |
698 | (season.i, dir.dir)) | |
699 | src = lookup(vseason.episodes, season.ep_i, | |
700 | "episode %d.%d not found in video dir `%s'" % | |
701 | (season.i, season.ep_i, dir.dir)) | |
702 | return src | |
151b3c4f MW |
703 | |
704 | def _process_cmd(me, ww): | |
705 | ||
706 | cmd = ww.nextword(); check(cmd is not None, "missing command") | |
707 | try: sep = cmd.index(":") | |
708 | except ValueError: opts = None | |
709 | else: cmd, opts = cmd[:sep], cmd[sep + 1:] | |
710 | ||
d1f1c578 MW |
711 | if cmd == "title": |
712 | for k, v in me._keyvals(opts): me._bad_keyval("title", k, v) | |
713 | title = ww.rest(); check(title is not None, "missing title") | |
714 | check(me._pl.series_title is None, "already set a title") | |
715 | me._pl.series_title = title | |
716 | ||
717 | elif cmd == "single": | |
718 | for k, v in me._keyvals(opts): me._bad_keyval("single", k, v) | |
719 | check(ww.rest() is None, "trailing junk") | |
720 | check(not me._pl.single_series_p, "single-series already set") | |
721 | me._pl.single_series_p = True | |
722 | ||
723 | elif cmd == "series": | |
151b3c4f MW |
724 | name = None |
725 | for k, v in me._keyvals(opts): | |
726 | if k is None: name = v | |
727 | else: me._bad_keyval(cmd, k, v) | |
728 | check(name is not None, "missing series name") | |
729 | check(name not in me._series, "series `%s' already defined" % name) | |
028b4b51 | 730 | title = ww.rest() |
2cf61248 MW |
731 | if title is None: |
732 | full = None | |
733 | else: | |
734 | try: sep = title.index("::") | |
735 | except ValueError: full = title | |
736 | else: | |
737 | full = title[sep + 2:].strip() | |
738 | if sep == 0: title = None | |
739 | else: title = title[:sep].strip() | |
151b3c4f | 740 | me._set_mode(MODE_MULTI) |
2cf61248 | 741 | me._series[name] = series = Series(me._pl, name, title, full, |
08f08e7c MW |
742 | me._series_wanted is None or |
743 | name in me._series_wanted) | |
744 | if series.wantedp: me._pl.nseries += 1 | |
151b3c4f MW |
745 | |
746 | elif cmd == "season": | |
08f08e7c | 747 | series = me._opts_series(cmd, opts) |
151b3c4f MW |
748 | w = ww.nextword(); |
749 | check(w is not None, "missing season number") | |
750 | if w == "-": | |
08f08e7c | 751 | if not series.wantedp: return |
c3538df6 | 752 | series.add_movies(ww.rest()) |
151b3c4f MW |
753 | else: |
754 | title = ww.rest(); i = getint(w) | |
08f08e7c | 755 | if not series.wantedp: return |
151b3c4f MW |
756 | series.add_season(ww.rest(), getint(w), implicitp = False) |
757 | me._cur_episode = me._cur_chapter = None | |
758 | me._pl.done_season() | |
759 | ||
066e5d43 MW |
760 | elif cmd == "explen": |
761 | w = ww.rest(); check(w is not None, "missing duration spec") | |
fe3e636c MW |
762 | if w == "-": |
763 | me._explen, me._expvar = None, DEFAULT_EXPVAR | |
764 | else: | |
765 | d, v = parse_duration(w) | |
766 | me._explen = d | |
767 | if v is not None: me._expvar = v | |
066e5d43 | 768 | |
151b3c4f MW |
769 | elif cmd == "epname": |
770 | for k, v in me._keyvals(opts): me._bad_keyval("epname", k, v) | |
771 | name = ww.rest(); check(name is not None, "missing episode name") | |
b06cf0d0 | 772 | try: sep = name.index("::") |
151b3c4f | 773 | except ValueError: names = name + "s" |
250b0ab7 | 774 | else: name, names = name[:sep], name[sep + 2:] |
151b3c4f MW |
775 | me._pl.epname, me._pl.epnames = name, names |
776 | ||
777 | elif cmd == "epno": | |
08f08e7c | 778 | series = me._opts_series(cmd, opts) |
151b3c4f MW |
779 | w = ww.rest(); check(w is not None, "missing episode number") |
780 | epi = getint(w) | |
08f08e7c | 781 | if not series.wantedp: return |
151b3c4f MW |
782 | series.ensure_season().ep_i = epi |
783 | ||
b16d5186 | 784 | elif cmd == "dvd": |
08f08e7c | 785 | series = me._opts_series(cmd, opts) |
151b3c4f | 786 | fn = ww.rest(); check(fn is not None, "missing filename") |
08f08e7c MW |
787 | if not series.wantedp: return |
788 | if fn == "-": forget(me._isos, series.name) | |
151b3c4f MW |
789 | else: |
790 | check(OS.path.exists(OS.path.join(ROOT, fn)), | |
7427f306 MW |
791 | "dvd iso file `%s' not found" % fn) |
792 | me._isos[series.name] = DVDFile(fn) | |
151b3c4f | 793 | |
b16d5186 | 794 | elif cmd == "dvddir": |
08f08e7c | 795 | series = me._opts_series(cmd, opts) |
151b3c4f | 796 | dir = ww.rest(); check(dir is not None, "missing directory") |
08f08e7c MW |
797 | if not series.wantedp: return |
798 | if dir == "-": forget(me._vdirs, series.name) | |
7427f306 | 799 | else: me._vdirs[series.name] = DVDDir(dir) |
151b3c4f | 800 | |
eaa841b2 MW |
801 | elif cmd == "vdir": |
802 | series = me._opts_series(cmd, opts) | |
803 | dir = ww.rest(); check(dir is not None, "missing directory") | |
804 | if not series.wantedp: return | |
805 | if dir == "-": forget(me._sfdirs, series.name) | |
806 | else: me._sfdirs[series.name] = VideoDir(dir) | |
807 | ||
151b3c4f | 808 | elif cmd == "adir": |
08f08e7c | 809 | series = me._opts_series(cmd, opts) |
151b3c4f | 810 | dir = ww.rest(); check(dir is not None, "missing directory") |
08f08e7c | 811 | if not series.wantedp: return |
7427f306 MW |
812 | if dir == "-": forget(me._sfdirs, series.name) |
813 | else: me._sfdirs[series.name] = AudioDir(dir) | |
04a05f7f | 814 | |
0c4ca4f3 | 815 | elif cmd == "displaced": |
08f08e7c | 816 | series = me._opts_series(cmd, opts) |
0c4ca4f3 | 817 | w = ww.rest(); check(w is not None, "missing count"); n = getint(w) |
08f08e7c | 818 | src = me._auto_epsrc(series) |
0c4ca4f3 | 819 | src.nuses += n |
066e5d43 | 820 | |
f8b9c373 MW |
821 | elif cmd == "sep": |
822 | sep = ww.rest(); check(sep is not None, "missing separator") | |
823 | me._pl.series_sep = sep | |
824 | ||
151b3c4f MW |
825 | else: |
826 | raise ExpectedError("unknown command `%s'" % cmd) | |
827 | ||
828 | def _process_episode(me, ww): | |
829 | ||
830 | opts = ww.nextword(); check(opts is not None, "missing title/options") | |
d4f60571 | 831 | ti = -1; sname = None; neps = 1; epi = None; loch = hich = -1 |
066e5d43 | 832 | explen, expvar, explicitlen = me._explen, me._expvar, False |
fb430389 | 833 | series_title_p = True |
151b3c4f MW |
834 | for k, v in me._keyvals(opts): |
835 | if k is None: | |
836 | if v.isdigit(): ti = int(v) | |
d4f60571 | 837 | elif v == "-": ti = -1 |
151b3c4f MW |
838 | else: sname = v |
839 | elif k == "s": sname = v | |
840 | elif k == "n": neps = getint(v) | |
841 | elif k == "ep": epi = getint(v) | |
fb430389 | 842 | elif k == "st": series_title_p = getbool(v) |
066e5d43 MW |
843 | elif k == "l": |
844 | if v == "-": me._explen, me._expvar = None, DEFAULT_EXPVAR | |
845 | else: | |
846 | explen, expvar = parse_duration(v, explen, expvar) | |
847 | explicitlen = True | |
0411af2c MW |
848 | elif k == "ch": |
849 | try: sep = v.index("-") | |
d4f60571 | 850 | except ValueError: loch, hich = getint(v), -1 |
0411af2c | 851 | else: loch, hich = getint(v[:sep]), getint(v[sep + 1:]) + 1 |
151b3c4f MW |
852 | else: raise ExpectedError("unknown episode option `%s'" % k) |
853 | check(ti is not None, "missing title number") | |
854 | series = me._get_series(sname) | |
855 | me._cur_chapter = None | |
856 | ||
857 | title = ww.rest() | |
08f08e7c | 858 | if not series.wantedp: return |
151b3c4f MW |
859 | season = series.ensure_season() |
860 | if epi is None: epi = season.ep_i | |
861 | ||
d4f60571 | 862 | if ti == -1: |
71305e44 MW |
863 | check(season.implicitp or season.i is None, |
864 | "audio source, but explicit non-movie season") | |
7427f306 MW |
865 | dir = lookup(me._sfdirs, series.name, |
866 | "no title, and no single-file directory") | |
151b3c4f | 867 | src = lookup(dir.episodes, season.ep_i, |
7427f306 MW |
868 | "episode %d not found in single-file dir `%s'" % |
869 | (epi, dir.dir)) | |
04a05f7f | 870 | |
151b3c4f | 871 | else: |
08f08e7c MW |
872 | try: src = me._isos[series.name] |
873 | except KeyError: src = me._auto_epsrc(series) | |
151b3c4f | 874 | |
fb430389 MW |
875 | episode = season.add_episode(epi, neps, title, src, |
876 | series_title_p, ti, loch, hich) | |
066e5d43 MW |
877 | |
878 | if episode.duration != -1 and explen is not None: | |
879 | if not explicitlen: explen *= neps | |
880 | if not explen*(1 - expvar) <= episode.duration <= explen*(1 + expvar): | |
881 | if season.i is None: epid = "episode %d" % epi | |
882 | else: epid = "episode %d.%d" % (season.i, epi) | |
883 | raise ExpectedError \ | |
884 | ("%s duration %s %g%% > %g%% from expected %s" % | |
885 | (epid, format_duration(episode.duration), | |
886 | abs(100*(episode.duration - explen)/explen), 100*expvar, | |
887 | format_duration(explen))) | |
151b3c4f MW |
888 | me._pl.add_episode(episode) |
889 | me._cur_episode = episode | |
890 | ||
891 | def _process_chapter(me, ww): | |
892 | check(me._cur_episode is not None, "no current episode") | |
893 | check(me._cur_episode.source.CHAPTERP, | |
894 | "episode source doesn't allow chapters") | |
895 | if me._chaptersp: | |
896 | if me._cur_chapter is None: i = 1 | |
897 | else: i = me._cur_chapter.i + 1 | |
898 | me._cur_chapter = me._cur_episode.add_chapter(ww.rest(), i) | |
899 | ||
900 | def parse_file(me, fn): | |
901 | with location(FileLocation(fn, 0)) as floc: | |
902 | with open(fn, "r") as f: | |
903 | for line in f: | |
904 | floc.stepline() | |
905 | sline = line.lstrip() | |
906 | if sline == "" or sline.startswith(";"): continue | |
907 | ||
908 | if line.startswith("!"): me._process_cmd(Words(line[1:])) | |
909 | elif not line[0].isspace(): me._process_episode(Words(line)) | |
910 | else: me._process_chapter(Words(line)) | |
911 | me._pl.done_season() | |
912 | ||
913 | def done(me): | |
914 | discs = set() | |
915 | for name, vdir in me._vdirs.items(): | |
08f08e7c | 916 | if not me._series[name].wantedp: continue |
151b3c4f MW |
917 | for s in vdir.seasons.values(): |
918 | for d in s.episodes.values(): | |
919 | discs.add(d) | |
7427f306 MW |
920 | for sfdir in me._sfdirs.values(): |
921 | for d in sfdir.episodes.values(): | |
b092d511 | 922 | discs.add(d) |
151b3c4f MW |
923 | for d in sorted(discs, key = lambda d: d.fn): |
924 | if d.neps is not None and d.neps != d.nuses: | |
925 | raise ExpectedError("disc `%s' has %d episodes, used %d times" % | |
926 | (d.fn, d.neps, d.nuses)) | |
927 | return me._pl | |
04a05f7f | 928 | |
151b3c4f | 929 | op = OP.OptionParser \ |
e6650127 | 930 | (usage = "%prog [-Dc] [-L NAME] [-M DEPS] [-d CACHE] [-o OUT] [-s SERIES] EPLS\n" |
1766dcfb | 931 | "%prog -i -d CACHE", |
151b3c4f | 932 | description = "Generate M3U playlists from an episode list.") |
1766dcfb MW |
933 | op.add_option("-D", "--dump", |
934 | dest = "dump", action = "store_true", default = False, | |
935 | help = "Dump playlist in machine-readable form") | |
e6650127 MW |
936 | op.add_option("-L", "--list-name", metavar = "NAME", |
937 | dest = "list_name", type = "str", default = None, | |
938 | help = "Set the playlist name") | |
2f4f35b0 MW |
939 | op.add_option("-M", "--make-deps", metavar = "DEPS", |
940 | dest = "deps", type = "str", default = None, | |
941 | help = "Write a `make' fragment for dependencies") | |
151b3c4f MW |
942 | op.add_option("-c", "--chapters", |
943 | dest = "chaptersp", action = "store_true", default = False, | |
944 | help = "Output individual chapter names") | |
0becf74e MW |
945 | op.add_option("-i", "--init-db", |
946 | dest = "initdbp", action = "store_true", default = False, | |
947 | help = "Initialize the database") | |
2f4f35b0 | 948 | op.add_option("-d", "--database", metavar = "CACHE", |
0becf74e MW |
949 | dest = "database", type = "str", default = None, |
950 | help = "Set filename for cache database") | |
2f4f35b0 MW |
951 | op.add_option("-o", "--output", metavar = "OUT", |
952 | dest = "output", type = "str", default = None, | |
953 | help = "Write output playlist to OUT") | |
954 | op.add_option("-O", "--fake-output", metavar = "OUT", | |
955 | dest = "fakeout", type = "str", default = None, | |
956 | help = "Pretend output goes to OUT for purposes of `-M'") | |
957 | op.add_option("-s", "--series", metavar = "SERIES", | |
151b3c4f MW |
958 | dest = "series", type = "str", default = None, |
959 | help = "Output only the listed SERIES (comma-separated)") | |
04a05f7f | 960 | try: |
0becf74e | 961 | opts, argv = op.parse_args() |
2f4f35b0 | 962 | |
0becf74e MW |
963 | if opts.initdbp: |
964 | if opts.chaptersp or opts.series is not None or \ | |
2f4f35b0 MW |
965 | opts.output is not None or opts.deps is not None or \ |
966 | opts.fakeout is not None or \ | |
0becf74e MW |
967 | opts.database is None or len(argv): |
968 | op.print_usage(file = SYS.stderr); SYS.exit(2) | |
969 | setup_db(opts.database) | |
2f4f35b0 | 970 | |
0becf74e MW |
971 | else: |
972 | if len(argv) != 1: op.print_usage(file = SYS.stderr); SYS.exit(2) | |
973 | if opts.database is not None: init_db(opts.database) | |
974 | if opts.series is None: | |
975 | series_wanted = None | |
976 | else: | |
977 | series_wanted = set() | |
978 | for name in opts.series.split(","): series_wanted.add(name) | |
2f4f35b0 MW |
979 | if opts.deps is not None: |
980 | if (opts.output is None or opts.output == "-") and opts.fakeout is None: | |
981 | raise ExpectedError("can't write dep fragment without output file") | |
982 | if opts.fakeout is None: opts.fakeout = opts.output | |
983 | else: | |
984 | if opts.fakeout is not None: | |
985 | raise ExpectedError("fake output set but no dep fragment") | |
986 | ||
0becf74e MW |
987 | ep = EpisodeListParser(series_wanted, opts.chaptersp) |
988 | ep.parse_file(argv[0]) | |
989 | pl = ep.done() | |
2f4f35b0 | 990 | |
e6650127 MW |
991 | if opts.list_name is None: |
992 | opts.list_name, _ = OS.path.splitext(OS.path.basename(argv[0])) | |
993 | ||
1766dcfb MW |
994 | if opts.dump: outfn = pl.dump |
995 | else: outfn = pl.write | |
2f4f35b0 | 996 | if opts.output is None or opts.output == "-": |
1766dcfb | 997 | outfn(SYS.stdout) |
2f4f35b0 | 998 | else: |
1766dcfb | 999 | with open(opts.output, "w") as f: outfn(f) |
2f4f35b0 MW |
1000 | |
1001 | if opts.deps: | |
1002 | if opts.deps == "-": | |
1003 | pl.write_deps(SYS.stdout, opts.fakeout) | |
1004 | else: | |
1005 | with open(opts.deps, "w") as f: pl.write_deps(f, opts.fakeout) | |
1006 | ||
04a05f7f MW |
1007 | except (ExpectedError, IOError, OSError) as e: |
1008 | LOC.report(e) | |
1009 | SYS.exit(2) |