### -*- mode: python; coding: utf-8 -*-
from contextlib import contextmanager
+import errno as E
import optparse as OP
import os as OS
import re as RX
+import sqlite3 as SQL
import subprocess as SP
import sys as SYS
if not s.isdigit(): raise ExpectedError("bad integer `%s'" % s)
return int(s)
+def getbool(s):
+ if s == "t": return True
+ elif s == "nil": return False
+ else: raise ExpectedError("bad boolean `%s'" % s)
+
class Words (object):
def __init__(me, s):
me._s = s
LOC = DummyLocation()
+ROOT = "/mnt/dvd/archive/"
+DB = None
+
+def init_db(fn):
+ global DB
+ DB = SQL.connect(fn)
+ DB.cursor().execute("PRAGMA journal_mode = WAL")
+
+def setup_db(fn):
+ try: OS.unlink(fn)
+ except OSError as e:
+ if e.errno == E.ENOENT: pass
+ else: raise
+ init_db(fn)
+ DB.cursor().execute("""
+ CREATE TABLE duration
+ (path TEXT NOT NULL,
+ title INTEGER NOT NULL,
+ start_chapter INTEGER NOT NULL,
+ end_chapter INTEGER NOT NULL,
+ inode INTEGER NOT NULL,
+ device INTEGER NOT NULL,
+ size INTEGER NOT NULL,
+ mtime REAL NOT NULL,
+ duration REAL NOT NULL,
+ PRIMARY KEY (path, title, start_chapter, end_chapter));
+ """)
+
class Source (object):
PREFIX = ""
TITLEP = CHAPTERP = False
else:
suffix = "#%d:%d-%d:%d" % (title, start_chapter, title, end_chapter - 1)
- duration = me._duration(title, start_chapter, end_chapter)
+ duration = None
+ if DB is None:
+ duration = me._duration(title, start_chapter, end_chapter)
+ else:
+ st = OS.stat(OS.path.join(ROOT, me.fn))
+ duration = None
+ c = DB.cursor()
+ c.execute("""
+ SELECT device, inode, size, mtime, duration FROM duration
+ WHERE path = ? AND title = ? AND
+ start_chapter = ? AND end_chapter = ?
+ """, [me.fn, title, start_chapter is None and -1 or start_chapter,
+ end_chapter is None and -1 or end_chapter])
+ row = c.fetchone()
+ foundp = False
+ if row is None:
+ duration = me._duration(title, start_chapter, end_chapter)
+ c.execute("""
+ INSERT OR REPLACE INTO duration
+ (path, title, start_chapter, end_chapter,
+ device, inode, size, mtime, duration)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, [me.fn, title, start_chapter is None and -1 or start_chapter,
+ end_chapter is None and -1 or end_chapter,
+ st.st_dev, st.st_ino, st.st_size, st.st_mtime,
+ duration])
+ else:
+ dev, ino, sz, mt, d = row
+ if (dev, ino, sz, mt) == \
+ (st.st_dev, st.st_ino, st.st_size, st.st_mtime):
+ duration = d
+ else:
+ duration = me._duration(title, start_chapter, end_chapter)
+ c.execute("""
+ UPDATE duration
+ SET device = ?, inode = ?, size = ?, mtime = ?, duration = ?
+ WHERE path = ? AND title = ? AND
+ start_chapter = ? AND end_chapter = ?
+ """, [st.st_dev, st.st_dev, st.st_size, st.st_mtime, duration,
+ me.fn, title, start_chapter is None and -1 or start_chapter,
+ end_chapter is None and -1 or end_chapter])
+ DB.commit()
if end_chapter is not None:
keys = [(title, ch) for ch in range(start_chapter, end_chapter)]
episode.source.url_and_duration(episode.tno, i, i + 1)
class Episode (object):
- def __init__(me, season, i, neps, title, src, tno = None,
- startch = None, endch = None):
+ def __init__(me, season, i, neps, title, src, series_title_p = True,
+ tno = None, startch = None, endch = None):
me.season = season
me.i, me.neps, me.title = i, neps, title
me.chapters = []
me.source, me.tno = src, tno
+ me.series_title_p = series_title_p
me.url, me.duration = src.url_and_duration(tno, startch, endch)
def add_chapter(me, title, j):
ch = Chapter(me, title, j)
me.episodes = []
me.implicitp = implicitp
me.ep_i, episodes = 1, []
- def add_episode(me, j, neps, title, src, tno, startch, endch):
- ep = Episode(me, j, neps, title, src, tno, startch, endch)
+ def add_episode(me, j, neps, title, src, series_title_p,
+ tno, startch, endch):
+ ep = Episode(me, j, neps, title, src, series_title_p,
+ tno, startch, endch)
me.episodes.append(ep)
src.nuses += neps; me.ep_i += neps
return ep
label = "%s %s" % \
(epname, ", ".join("%d.%s" % (me.i, e) for e in epn))
else:
- label = "%s—%s %s" % (me.title, epname, ", ".join(epn))
+ label = "%s: %s %s" % (me.title, epname, ", ".join(epn))
else:
if me.implicitp:
label = "%s. %s" % (", ".join(epn), title)
label = "%s. %s" % \
(", ".join("%d.%s" % (me.i, e) for e in epn), title)
else:
- label = "%s—%s. %s" % (me.title, ", ".join(epn), title)
+ label = "%s: %s. %s" % (me.title, ", ".join(epn), title)
return label
class MovieSeason (BaseSeason):
super().__init__(series, *args, **kw)
me.title = title
me.i = None
- def add_episode(me, j, neps, title, src, tno, startch, endch):
+ def add_episode(me, j, neps, title, src, series_title_p,
+ tno, startch, endch):
if me.title is None and title is None:
raise ExpectedError("movie or movie season must have a title")
- return super().add_episode(j, neps, title, src, tno, startch, endch)
+ return super().add_episode(j, neps, title, src, series_title_p,
+ tno, startch, endch)
def _eplabel(me, i, neps, title):
if me.title is None:
label = title
elif title is None:
epname, epn = me._epnames(i, neps)
- label = "%s—%s %s" % (me.title, epname, ", ".join(epn))
+ label = "%s: %s %s" % (me.title, epname, ", ".join(epn))
else:
- label = "%s—%s" % (me.title, title)
+ label = "%s: %s" % (me.title, title)
return label
class Series (object):
if me.episodes:
me.seasons.append(me.episodes)
me.episodes = []
+
def write(me, f):
f.write("#EXTM3U\n")
for season in me.seasons:
f.write("\n")
for ep in season:
label = ep.label()
- if me.nseries > 1: label = ep.season.series.title + " " + label
+ if me.nseries > 1 and ep.series_title_p and \
+ ep.season.series.title is not None:
+ if ep.season.i is None: sep = ": "
+ else: sep = " "
+ label = ep.season.series.title + sep + label
if not ep.chapters:
f.write("#EXTINF:%d,,%s\n%s\n" % (ep.duration, label, ep.url))
else:
f.write("#EXTINF:%d,,%s: %s\n%s\n" %
(ch.duration, label, ch.title, ch.url))
+ 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
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(); check(title is not None, "missing title")
+ title = ww.rest()
me._set_mode(MODE_MULTI)
me._series[name] = series = Series(me._pl, name, title,
me._series_wanted is None or
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
+ 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(",")
+ try: sep = name.index("::")
except ValueError: names = name + "s"
else: name, names = name[:sep], name[sep + 1:]
me._pl.epname, me._pl.epnames = name, names
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
+ series_title_p = True
for k, v in me._keyvals(opts):
if k is None:
if v.isdigit(): ti = int(v)
elif k == "s": sname = v
elif k == "n": neps = getint(v)
elif k == "ep": epi = getint(v)
+ elif k == "st": series_title_p = getbool(v)
elif k == "l":
if v == "-": me._explen, me._expvar = None, DEFAULT_EXPVAR
else:
try: src = me._isos[series.name]
except KeyError: src = me._auto_epsrc(series)
- episode = season.add_episode(epi, neps, title, src, ti, loch, hich)
+ episode = season.add_episode(epi, neps, title, src,
+ series_title_p, ti, loch, hich)
if episode.duration != -1 and explen is not None:
if not explicitlen: explen *= neps
(d.fn, d.neps, d.nuses))
return me._pl
-ROOT = "/mnt/dvd/archive/"
-
op = OP.OptionParser \
- (usage = "%prog [-c] [-s SERIES] EPLS",
+ (usage = "%prog [-c] [-M DEPS] [-d CACHE] [-o OUT] [-s SERIES] EPLS\n"
+ "%prog -i -d CACHE",
description = "Generate M3U playlists from an episode list.")
+op.add_option("-M", "--make-deps", metavar = "DEPS",
+ dest = "deps", type = "str", default = None,
+ help = "Write a `make' fragment for dependencies")
op.add_option("-c", "--chapters",
dest = "chaptersp", action = "store_true", default = False,
help = "Output individual chapter names")
-op.add_option("-s", "--series",
+op.add_option("-i", "--init-db",
+ dest = "initdbp", action = "store_true", default = False,
+ help = "Initialize the database")
+op.add_option("-d", "--database", metavar = "CACHE",
+ dest = "database", type = "str", default = None,
+ help = "Set filename for cache database")
+op.add_option("-o", "--output", metavar = "OUT",
+ dest = "output", type = "str", default = None,
+ help = "Write output playlist to OUT")
+op.add_option("-O", "--fake-output", metavar = "OUT",
+ dest = "fakeout", type = "str", default = None,
+ help = "Pretend output goes to OUT for purposes of `-M'")
+op.add_option("-s", "--series", metavar = "SERIES",
dest = "series", type = "str", default = None,
help = "Output only the listed SERIES (comma-separated)")
-opts, argv = op.parse_args()
-if len(argv) != 1: op.print_usage(file = SYS.stderr); SYS.exit(2)
-if opts.series is None:
- series_wanted = None
-else:
- series_wanted = set()
- for name in opts.series.split(","): series_wanted.add(name)
try:
- ep = EpisodeListParser(series_wanted, opts.chaptersp)
- ep.parse_file(argv[0])
- pl = ep.done()
- pl.write(SYS.stdout)
+ opts, argv = op.parse_args()
+
+ if opts.initdbp:
+ if opts.chaptersp or opts.series is not None or \
+ opts.output is not None or opts.deps is not None or \
+ opts.fakeout is not None or \
+ opts.database is None or len(argv):
+ op.print_usage(file = SYS.stderr); SYS.exit(2)
+ setup_db(opts.database)
+
+ else:
+ if len(argv) != 1: op.print_usage(file = SYS.stderr); SYS.exit(2)
+ if opts.database is not None: init_db(opts.database)
+ if opts.series is None:
+ series_wanted = None
+ else:
+ series_wanted = set()
+ for name in opts.series.split(","): series_wanted.add(name)
+ if opts.deps is not None:
+ if (opts.output is None or opts.output == "-") and opts.fakeout is None:
+ raise ExpectedError("can't write dep fragment without output file")
+ if opts.fakeout is None: opts.fakeout = opts.output
+ else:
+ if opts.fakeout is not None:
+ raise ExpectedError("fake output set but no dep fragment")
+
+ ep = EpisodeListParser(series_wanted, opts.chaptersp)
+ ep.parse_file(argv[0])
+ pl = ep.done()
+
+ if opts.output is None or opts.output == "-":
+ pl.write(SYS.stdout)
+ else:
+ with open(opts.output, "w") as f: pl.write(f)
+
+ if opts.deps:
+ if opts.deps == "-":
+ pl.write_deps(SYS.stdout, opts.fakeout)
+ else:
+ with open(opts.deps, "w") as f: pl.write_deps(f, opts.fakeout)
+
except (ExpectedError, IOError, OSError) as e:
LOC.report(e)
SYS.exit(2)