From 0becf74e51f9f98a08c86c37cd492eb3be138e7e Mon Sep 17 00:00:00 2001 Message-Id: <0becf74e51f9f98a08c86c37cd492eb3be138e7e.1718939914.git.mdw@distorted.org.uk> From: Mark Wooding Date: Tue, 22 Mar 2022 00:27:00 +0000 Subject: [PATCH] mkm3u: Maintain a cache of durations because they take ages to look up. Organization: Straylight/Edgeware From: Mark Wooding --- .gitignore | 4 ++ Makefile | 18 ++++++++- mkm3u | 113 ++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 909c9bc..0940b00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.m3u8 +*.m3u8.new !/ref/*.m3u8 + +/mkm3u.cache +/mkm3u.cache-stamp diff --git a/Makefile b/Makefile index 4cf4bb6..33136ba 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ all: clean::; rm -f $(CLEANFILES) +realclean::; rm -f $(REALCLEANFILES) force: .PHONY: all clean .SECONDEXPANSION: # not sorry @@ -17,6 +18,10 @@ v-tag.0 = @$(call v-print.0,$1,$@); TARGETS = CLEANFILES += $(TARGETS) +REALCLEANFILES += $(CLEANFILES) + +MKM3UFLAGS = -dmkm3u.cache +REALCLEANFILES += mkm3u.cache define %declare-playlist PLAYLISTS += $1 @@ -175,8 +180,17 @@ $(call declare-playlist, drwho-silurians, D/Doctor Who/S07E02 BBB. Doctor Who an M3US = $(addsuffix .m3u8,$(PLAYLISTS)) TARGETS += $(M3US) -$(M3US): %.m3u8: $$($$*_EPLS) mkm3u - $(call v-tag,MKM3U)./mkm3u $($*_MKM3UFLAGS) "$<" >"$@.new" && mv "$@.new" "$@" +CLEANFILES += mkm3u.cache-stamp +mkm3u.cache-stamp: + if [ ! -f mkm3u.cache ]; then \ + ./mkm3u -i -dmkm3u.cache.new && mv mkm3u.cache.new mkm3u.cache; \ + fi + touch $@ + +CLEANFILES += *.m3u8.new +$(M3US): %.m3u8: $$($$*_EPLS) mkm3u mkm3u.cache-stamp + $(call v-tag,MKM3U)./mkm3u $(MKM3UFLAGS) $($*_MKM3UFLAGS) \ + "$<" >"$@.new" && mv "$@.new" "$@" CHECKS = $(foreach p,$(PLAYLISTS), check/$p) check: $(CHECKS) diff --git a/mkm3u b/mkm3u index 5464ac7..9053c5f 100755 --- a/mkm3u +++ b/mkm3u @@ -2,9 +2,11 @@ ### -*- 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 @@ -97,6 +99,34 @@ class FileLocation (BaseLocation): 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 @@ -128,7 +158,48 @@ class Source (object): 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)] @@ -737,29 +808,41 @@ class EpisodeListParser (object): (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] [-d CACHE] [-s SERIES] EPLS\n" + "%prog -i -d CACHE", description = "Generate M3U playlists from an episode list.") op.add_option("-c", "--chapters", dest = "chaptersp", action = "store_true", default = False, help = "Output individual chapter names") +op.add_option("-i", "--init-db", + dest = "initdbp", action = "store_true", default = False, + help = "Initialize the database") +op.add_option("-d", "--database", + dest = "database", type = "str", default = None, + help = "Set filename for cache database") op.add_option("-s", "--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.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) + ep = EpisodeListParser(series_wanted, opts.chaptersp) + ep.parse_file(argv[0]) + pl = ep.done() + pl.write(SYS.stdout) except (ExpectedError, IOError, OSError) as e: LOC.report(e) SYS.exit(2) -- [mdw]