mkm3u, *.epls: Check that episode lengths are within expected bounds.
[epls] / mkm3u
diff --git a/mkm3u b/mkm3u
index 32b9bf2..982475c 100755 (executable)
--- a/mkm3u
+++ b/mkm3u
@@ -437,6 +437,29 @@ class Playlist (object):
             f.write("#EXTINF:%d,,%s: %s\n%s\n" %
                     (ch.duration, label, ch.title, ch.url))
 
+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
@@ -449,6 +472,7 @@ class EpisodeListParser (object):
     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
 
@@ -535,6 +559,12 @@ class EpisodeListParser (object):
       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")
+      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")
@@ -579,6 +609,7 @@ class EpisodeListParser (object):
       w = ww.rest(); check(w is not None, "missing count"); n = getint(w)
       src = me._auto_epsrc(series)
       src.nuses += n
+
     else:
       raise ExpectedError("unknown command `%s'" % cmd)
 
@@ -586,6 +617,7 @@ class EpisodeListParser (object):
 
     opts = ww.nextword(); check(opts is not None, "missing title/options")
     ti = None; sname = None; neps = 1; epi = None; loch = hich = None
+    explen, expvar, explicitlen = me._explen, me._expvar, False
     for k, v in me._keyvals(opts):
       if k is None:
         if v.isdigit(): ti = int(v)
@@ -594,6 +626,11 @@ class EpisodeListParser (object):
       elif k == "s": sname = v
       elif k == "n": neps = getint(v)
       elif k == "ep": epi = getint(v)
+      elif k == "l":
+        if v == "-": me._explen, me._expvar = None, DEFAULT_EXPVAR
+        else:
+          explen, expvar = parse_duration(v, explen, expvar)
+          explicitlen = True
       elif k == "ch":
         try: sep = v.index("-")
         except ValueError: loch, hich = getint(v), None
@@ -620,6 +657,17 @@ class EpisodeListParser (object):
       except KeyError: src = me._auto_epsrc(series)
 
     episode = season.add_episode(epi, neps, title, src, ti, loch, hich)
+
+    if episode.duration != -1 and explen is not None:
+      if not explicitlen: explen *= neps
+      if not explen*(1 - expvar) <= episode.duration <= explen*(1 + expvar):
+        if season.i is None: epid = "episode %d" % epi
+        else: epid = "episode %d.%d" % (season.i, epi)
+        raise ExpectedError \
+          ("%s duration %s %g%% > %g%% from expected %s" %
+             (epid, format_duration(episode.duration),
+              abs(100*(episode.duration - explen)/explen), 100*expvar,
+              format_duration(explen)))
     me._pl.add_episode(episode)
     me._cur_episode = episode