- f.write("#EXTINF:0,,%s: %s\n%s\n" %
- (ep.label(), ch.title, ch.url))
-
-UNSET = ["UNSET"]
-
-def parse_list(fn):
- playlist = Playlist()
- season, episode, chapter, ep_i = None, None, None, 1
- vds = {}
- ads = iso = None
- with location(FileLocation(fn, 0)) as floc:
- with open(fn, "r") as f:
- for line in f:
- floc.stepline()
- sline = line.lstrip()
- if sline == "" or sline.startswith(";"): continue
-
- if line.startswith("!"):
- ww = Words(line[1:])
- cmd = ww.nextword()
- check(cmd is not None, "missing command")
-
- if cmd == "season":
- v = ww.nextword();
- check(v is not None, "missing season number")
- if v == "-":
- check(v.rest() is None, "trailing junk")
- season = playlist.add_movies()
- else:
- i = getint(v)
- title = ww.rest()
- season = playlist.add_season(title, i, implicitp = False)
- episode = chapter = None
- ep_i = 1
-
- elif cmd == "movie":
- check(ww.rest() is None, "trailing junk")
- season = playlist.add_movies()
- episode = chapter = None
- ep_i = 1
-
- elif cmd == "epname":
- name = ww.rest()
- check(name is not None, "missing episode name")
- try: sep = name.index(":")
- except ValueError: names = name + "s"
- else: name, names = name[:sep], name[sep + 1:]
- playlist.epname, playlist.epnames = name, names
-
- elif cmd == "epno":
- i = ww.rest()
- check(i is not None, "missing episode number")
- ep_i = getint(i)
-
- elif cmd == "iso":
- fn = ww.rest(); check(fn is not None, "missing filename")
- if fn == "-": iso = None
- else:
- check(OS.path.exists(OS.path.join(ROOT, fn)),
- "iso file `%s' not found" % fn)
- iso = VideoDisc(fn)
-
- elif cmd == "vdir":
- name = ww.nextword(); check(name is not None, "missing name")
- fn = ww.rest(); check(fn is not None, "missing directory")
- if fn == "-":
- try: del vds[name]
- except KeyError: pass
- else:
- vds[name] = VideoDir(fn)
-
- elif cmd == "adir":
- fn = ww.rest(); check(fn is not None, "missing directory")
- if fn == "-": ads = None
- else: ads = AudioDir(fn)
-
- elif cmd == "end":
- break
-
- else:
- raise ExpectedError("unknown command `%s'" % cmd)
+ f.write("#EXTINF:%d,,%s: %s\n%s\n" %
+ (ch.duration, label, ch.title, ch.url))
+
+ def dump(me, f):
+ if opts.list_name is not None: f.write("LIST %s\n" % opts.list_name)
+ if me.series_title is not None and \
+ me.nseries > 1 and not me.single_series_p:
+ raise ExpectedError("can't force series name for multi-series list")
+ series = set()
+ if me.single_series_p:
+ f.write("SERIES - %s\n" % quote(me.series_title))
+ for season in me.seasons:
+ for ep in season:
+ label = ep.label()
+ title = ep.season.series.full_title
+ if me.single_series_p:
+ stag = "-"
+ if title is not None: label = title + " " + label
+ else:
+ if title is None: title = me.series_title
+ stag = ep.season.series.name
+ if stag is None: stag = "-"
+ if stag not in series:
+ f.write("SERIES %s %s\n" % (stag, quote(title)))
+ series.add(stag)
+ f.write("ENTRY %s %s %s %d %d %d %g\n" %
+ (stag, quote(label), quote(ep.source.fn),
+ ep.tno, ep.start_chapter, ep.end_chapter, ep.duration))
+
+ def write_deps(me, f, out):
+ deps = set()
+ for season in me.seasons:
+ for ep in season: deps.add(ep.source.fn)
+ f.write("### -*-makefile-*-\n")
+ f.write("%s: $(call check-deps, %s," % (out, out))
+ for dep in sorted(deps):
+ f.write(" \\\n\t'%s'" %
+ OS.path.join(ROOT, dep)
+ .replace(",", "$(comma)")
+ .replace("'", "'\\''"))
+ f.write(")\n")
+
+DEFAULT_EXPVAR = 0.05
+R_DURMULT = RX.compile(r""" ^
+ (\d+ (?: \. \d+)?) x
+$ """, RX.X)
+R_DUR = RX.compile(r""" ^
+ (?: (?: (\d+) :)? (\d+) :)? (\d+)
+ (?: / (\d+ (?: \. \d+)?) \%)?
+$ """, RX.X)
+def parse_duration(s, base = None, basevar = DEFAULT_EXPVAR):
+ if base is not None:
+ m = R_DURMULT.match(s)
+ if m is not None: return base*float(m.group(1)), basevar
+ m = R_DUR.match(s)
+ if not m: raise ExpectedError("invalid duration spec `%s'" % s)
+ hr, min, sec = map(lambda g: filter(m.group(g), int, 0), [1, 2, 3])
+ var = filter(m.group(4), lambda x: float(x)/100.0)
+ if var is None: var = DEFAULT_EXPVAR
+ return 3600*hr + 60*min + sec, var
+def format_duration(d):
+ if d >= 3600: return "%d:%02d:%02d" % (d//3600, (d//60)%60, d%60)
+ elif d >= 60: return "%d:%02d" % (d//60, d%60)
+ else: return "%d s" % d
+
+MODE_UNSET = 0
+MODE_SINGLE = 1
+MODE_MULTI = 2
+
+class EpisodeListParser (object):
+
+ def __init__(me, series_wanted = None, chapters_wanted_p = False):
+ me._pl = Playlist()
+ me._cur_episode = me._cur_chapter = None
+ me._series = {}; me._vdirs = {}; me._audirs = {}; me._isos = {}
+ me._series_wanted = series_wanted
+ me._chaptersp = chapters_wanted_p
+ me._explen, me._expvar = None, DEFAULT_EXPVAR
+ if series_wanted is None: me._mode = MODE_UNSET
+ else: me._mode = MODE_MULTI
+
+ def _bad_keyval(me, cmd, k, v):
+ raise ExpectedError("invalid `!%s' option `%s'" %
+ (cmd, v if k is None else k))
+
+ def _keyvals(me, opts):
+ if opts is not None:
+ for kv in opts.split(","):
+ try: sep = kv.index("=")
+ except ValueError: yield None, kv
+ else: yield kv[:sep], kv[sep + 1:]
+
+ def _set_mode(me, mode):
+ if me._mode == MODE_UNSET:
+ me._mode = mode
+ elif me._mode != mode:
+ raise ExpectedError("inconsistent single-/multi-series usage")
+
+ def _get_series(me, name):
+ if name is None:
+ me._set_mode(MODE_SINGLE)
+ try: series = me._series[None]
+ except KeyError:
+ series = me._series[None] = Series(me._pl, None)
+ me._pl.nseries += 1
+ else:
+ me._set_mode(MODE_MULTI)
+ series = lookup(me._series, name, "unknown series `%s'" % name)
+ return series
+
+ def _opts_series(me, cmd, opts):
+ name = None
+ for k, v in me._keyvals(opts):
+ if k is None: name = v
+ else: me._bad_keyval(cmd, k, v)
+ return me._get_series(name)
+
+ def _auto_epsrc(me, series):
+ dir = lookup(me._vdirs, series.name, "no active video directory")
+ season = series.ensure_season()
+ check(season.i is not None, "must use explicit iso for movie seasons")
+ vseason = lookup(dir.seasons, season.i,
+ "season %d not found in video dir `%s'" %
+ (season.i, dir.dir))
+ src = lookup(vseason.episodes, season.ep_i,
+ "episode %d.%d not found in video dir `%s'" %
+ (season.i, season.ep_i, dir.dir))
+ return src
+
+ def _process_cmd(me, ww):
+
+ cmd = ww.nextword(); check(cmd is not None, "missing command")
+ try: sep = cmd.index(":")
+ except ValueError: opts = None
+ else: cmd, opts = cmd[:sep], cmd[sep + 1:]
+
+ if cmd == "title":
+ for k, v in me._keyvals(opts): me._bad_keyval("title", k, v)
+ title = ww.rest(); check(title is not None, "missing title")
+ check(me._pl.series_title is None, "already set a title")
+ me._pl.series_title = title
+
+ elif cmd == "single":
+ for k, v in me._keyvals(opts): me._bad_keyval("single", k, v)
+ check(ww.rest() is None, "trailing junk")
+ check(not me._pl.single_series_p, "single-series already set")
+ me._pl.single_series_p = True
+
+ elif cmd == "series":
+ name = None
+ for k, v in me._keyvals(opts):
+ if k is None: name = v
+ else: me._bad_keyval(cmd, k, v)
+ check(name is not None, "missing series name")
+ check(name not in me._series, "series `%s' already defined" % name)
+ title = ww.rest()
+ if title is None:
+ full = None
+ else:
+ try: sep = title.index("::")
+ except ValueError: full = title
+ else:
+ full = title[sep + 2:].strip()
+ if sep == 0: title = None
+ else: title = title[:sep].strip()
+ me._set_mode(MODE_MULTI)
+ me._series[name] = series = Series(me._pl, name, title, full,
+ me._series_wanted is None or
+ name in me._series_wanted)
+ if series.wantedp: me._pl.nseries += 1
+
+ elif cmd == "season":
+ series = me._opts_series(cmd, opts)
+ w = ww.nextword();
+ check(w is not None, "missing season number")
+ if w == "-":
+ if not series.wantedp: return
+ series.add_movies(ww.rest())
+ else:
+ title = ww.rest(); i = getint(w)
+ if not series.wantedp: return
+ series.add_season(ww.rest(), getint(w), implicitp = False)
+ me._cur_episode = me._cur_chapter = None
+ me._pl.done_season()
+
+ elif cmd == "explen":
+ w = ww.rest(); check(w is not None, "missing duration spec")
+ if w == "-":
+ me._explen, me._expvar = None, DEFAULT_EXPVAR
+ else:
+ d, v = parse_duration(w)
+ me._explen = d
+ if v is not None: me._expvar = v
+
+ elif cmd == "epname":
+ for k, v in me._keyvals(opts): me._bad_keyval("epname", k, v)
+ name = ww.rest(); check(name is not None, "missing episode name")
+ try: sep = name.index("::")
+ except ValueError: names = name + "s"
+ else: name, names = name[:sep], name[sep + 2:]
+ me._pl.epname, me._pl.epnames = name, names
+
+ elif cmd == "epno":
+ series = me._opts_series(cmd, opts)
+ w = ww.rest(); check(w is not None, "missing episode number")
+ epi = getint(w)
+ if not series.wantedp: return
+ series.ensure_season().ep_i = epi
+
+ elif cmd == "iso":
+ series = me._opts_series(cmd, opts)
+ fn = ww.rest(); check(fn is not None, "missing filename")
+ if not series.wantedp: return
+ if fn == "-": forget(me._isos, series.name)
+ else:
+ check(OS.path.exists(OS.path.join(ROOT, fn)),
+ "iso file `%s' not found" % fn)
+ me._isos[series.name] = VideoDisc(fn)
+
+ elif cmd == "vdir":
+ series = me._opts_series(cmd, opts)
+ dir = ww.rest(); check(dir is not None, "missing directory")
+ if not series.wantedp: return
+ if dir == "-": forget(me._vdirs, series.name)
+ else: me._vdirs[series.name] = VideoDir(dir)
+
+ elif cmd == "adir":
+ series = me._opts_series(cmd, opts)
+ dir = ww.rest(); check(dir is not None, "missing directory")
+ if not series.wantedp: return
+ if dir == "-": forget(me._audirs, series.name)
+ else: me._audirs[series.name] = AudioDir(dir)
+
+ elif cmd == "displaced":
+ series = me._opts_series(cmd, opts)
+ w = ww.rest(); check(w is not None, "missing count"); n = getint(w)
+ src = me._auto_epsrc(series)
+ src.nuses += n