From: James Bunton Date: Mon, 18 Feb 2019 01:43:38 +0000 (+1100) Subject: MythTV maintenance scripts X-Git-Url: https://code.delx.au/mediapc-tools/commitdiff_plain/c9466a03237b26d8265a0bdc3b804f0a895c1306 MythTV maintenance scripts mythcleandb -- delete orphaned DB recordings that are missing the corresponding files mythcleanfiles -- delete orphaned recording files that are missing from the DB mythcleanpng -- delete orphaned recording screenshots mythsymlink -- update to python2 and use shared code --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/mythcleandb b/mythcleandb new file mode 100755 index 0000000..6e000fb --- /dev/null +++ b/mythcleandb @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import os +import sys + +import mythlib + +def main(dry_run): + config_xml = mythlib.get_config() + db_connection = mythlib.get_db_connection(config_xml) + recordings_dir = mythlib.get_recordings_dir(config_xml, db_connection) + + orphan_db_records = list(find_orphan_db_records(db_connection, recordings_dir)) + for basename in orphan_db_records: + if dry_run: + print("Found orphan:", basename) + else: + print("Deleting orphan:", basename) + delete_orphan_record(db_connection, basename) + +def print_usage_exit(): + print("Usage: %s [-f|-n]" % sys.argv[0]) + sys.exit(1) + +def find_orphan_db_records(db_connection, recordings_dir): + with db_connection.cursor() as cursor: + cursor.execute(""" + SELECT basename + FROM recorded + WHERE progend < now() + """) + for row in cursor: + basename = row[0] + if not is_valid_record(recordings_dir, basename): + yield basename + +def is_valid_record(recordings_dir, basename): + filename = "%s/%s" % (recordings_dir, basename) + try: + size = os.stat(filename).st_size + except FileNotFoundError: + size = 0 + + return size > 0 + +def delete_orphan_record(db_connection, basename): + with db_connection.cursor() as cursor: + cursor.execute("delete from recorded where basename='%s'" % basename) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print_usage_exit() + + if sys.argv[1] == "-f": + dry_run = False + elif sys.argv[1] == "-n": + dry_run = True + else: + print_usage_exit() + + main(dry_run) diff --git a/mythcleanfiles b/mythcleanfiles new file mode 100755 index 0000000..5c259c8 --- /dev/null +++ b/mythcleanfiles @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +import os +import sys + +import mythlib + +def main(): + config_xml = mythlib.get_config() + db_connection = mythlib.get_db_connection(config_xml) + recordings_dir = mythlib.get_recordings_dir(config_xml, db_connection) + + basenames = set(fetch_all_basenames(db_connection)) + orphan_filenames = list(find_orphan_filenames(recordings_dir, basenames)) + + if orphan_filenames: + print("# Orphaned files:") + print("\n".join("rm %s" % filename for filename in orphan_filenames)) + print() + +def fetch_all_basenames(db_connection): + with db_connection.cursor() as cursor: + cursor.execute("SELECT basename FROM recorded") + for row in cursor: + yield row[0] + +def find_orphan_filenames(recordings_dir, basenames): + for filename in os.listdir(recordings_dir): + _, ext = os.path.splitext(filename) + if ext not in [".mpg", ".ts"]: + continue + + if filename not in basenames: + yield "%s/%s" % (recordings_dir, filename) + +if __name__ == "__main__": + main() diff --git a/mythcleanpng b/mythcleanpng new file mode 100755 index 0000000..1679ee7 --- /dev/null +++ b/mythcleanpng @@ -0,0 +1,9 @@ +#!/bin/bash + +set -eu + +cd /var/lib/mythtv/recordings + +ls *.png.* &> /dev/null && rm -fv *.png.* + +for f in *.png; do [ -f "${f%%.png}" ] || rm -fv "$f"; done diff --git a/mythlib.py b/mythlib.py new file mode 100644 index 0000000..747e7c7 --- /dev/null +++ b/mythlib.py @@ -0,0 +1,34 @@ +#! python3 + +import os +import socket + +import lxml.etree +import MySQLdb + +def get_config(): + with open(os.path.expanduser("~/.mythtv/config.xml")) as f: + return lxml.etree.parse(f, lxml.etree.XMLParser(encoding="utf-8")) + +def get_db_connection(config_xml): + return MySQLdb.connect( + host = config_xml.xpath("/Configuration/Database/Host/text()")[0], + port = int(config_xml.xpath("/Configuration/Database/Port/text()")[0]), + user = config_xml.xpath("/Configuration/Database/UserName/text()")[0], + passwd = config_xml.xpath("/Configuration/Database/Password/text()")[0], + db = config_xml.xpath("/Configuration/Database/DatabaseName/text()")[0], + ) + +def get_recordings_dir(config_xml, db_connection): + try: + localhostname = config_xml.xpath("/Configuration/LocalHostName/text()")[0] + except IndexError: + localhostname = socket.gethostname() + + with db_connection.cursor(MySQLdb.cursors.DictCursor) as cursor: + cursor.execute(""" + SELECT * FROM settings + WHERE value='RecordFilePrefix' AND hostname='%s' + """ % localhostname) + + return cursor.fetchone()["data"] diff --git a/mythsymlink b/mythsymlink index 2bd754e..4870997 100755 --- a/mythsymlink +++ b/mythsymlink @@ -1,73 +1,57 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 +import MySQLdb import os import random import re -import socket import sys -import lxml.etree -import MySQLdb - -os.chdir(sys.argv[1]) - -# Remove any symlinks and empty dirs in the tree -for dirpath, dirnames, filenames in os.walk(".", topdown=False): - for filename in filenames: - filename = os.path.join(dirpath, filename) - if os.path.islink(filename): - os.unlink(filename) - for dirname in dirnames: - dirname = os.path.join(dirpath, dirname) - os.rmdir(dirname) - -# Connect to the MythTV database based on the MythTV config -with open(os.path.expanduser("~/.mythtv/config.xml")) as f: - config_xml = lxml.etree.parse(f, lxml.etree.XMLParser(encoding="utf-8")) - -db_connection = MySQLdb.connect( - host = config_xml.xpath("/Configuration/Database/Host/text()")[0], - port = int(config_xml.xpath("/Configuration/Database/Port/text()")[0]), - user = config_xml.xpath("/Configuration/Database/UserName/text()")[0], - passwd = config_xml.xpath("/Configuration/Database/Password/text()")[0], - db = config_xml.xpath("/Configuration/Database/DatabaseName/text()")[0], -) -cursor = db_connection.cursor(MySQLdb.cursors.DictCursor) - -# Regexp for what is allowed in the symlink name -unsafe = re.compile("[^a-zA-Z0-9\-_ ,\\.]+") - -try: - localhostname = config_xml.xpath("/Configuration/LocalHostName/text()")[0] -except IndexError: - localhostname = socket.gethostname() - -# Find the recordings directory -cursor.execute(""" - SELECT * FROM settings - WHERE value='RecordFilePrefix' AND hostname='%s' -""" % localhostname) -recordingsdir = cursor.fetchone()["data"] - -# Now find all the recordings we have at the moment -cursor.execute(""" - SELECT - title, - subtitle, - CONVERT_TZ(starttime, 'UTC', '%s') as starttime, - basename, - watched - FROM recorded -""" % "Australia/Sydney") -for row in cursor: +import mythlib + +def main(destdir): + os.chdir(destdir) + + clean_dir() + + config_xml = mythlib.get_config() + db_connection = mythlib.get_db_connection(config_xml) + recordings_dir = mythlib.get_recordings_dir(config_xml, db_connection) + + for_each_recording(db_connection, handle_recording, recordings_dir) + +def clean_dir(): + for dirpath, dirnames, filenames in os.walk(".", topdown=False): + for filename in filenames: + filename = os.path.join(dirpath, filename) + if os.path.islink(filename): + os.unlink(filename) + for dirname in dirnames: + dirname = os.path.join(dirpath, dirname) + os.rmdir(dirname) + +def for_each_recording(db_connection, fn, *args): + with db_connection.cursor(MySQLdb.cursors.DictCursor) as cursor: + cursor.execute(""" + SELECT + title, + subtitle, + CONVERT_TZ(starttime, 'UTC', '%s') as starttime, + basename, + watched + FROM recorded + """ % "Australia/Sydney") + for row in cursor: + fn(row, *args) + +def handle_recording(row, recordings_dir): title = row["title"] starttime = str(row["starttime"]).replace(":", "-") subtitle = row["subtitle"] basename = row["basename"] watched = bool(row["watched"]) - title = unsafe.sub("", title) - subtitle = unsafe.sub("", subtitle) + title = sanitize_filename(title) + subtitle = sanitize_filename(subtitle) extn = os.path.splitext(basename)[1] @@ -78,23 +62,36 @@ for row in cursor: if watched: filename = "watched/" + filename - source = "%s/%s" % (recordingsdir, basename) + source = "%s/%s" % (recordings_dir, basename) dest = "%s/%s" % (title, filename) if not os.path.isfile(source): - continue + return if os.path.isfile(dest): - dest = os.path.splitext(dest)[0] + ' - unique' + str(random.randint(1000, 9999)) + extn + dest = os.path.splitext(dest)[0] + " - unique" + str(random.randint(1000, 9999)) + extn dirnames = dest.split("/")[:-1] - for i in xrange(1, len(dirnames)+1): + for i in range(1, len(dirnames)+1): dirname = "/".join(dirnames[:i]) if not os.path.isdir(dirname): os.mkdir(dirname) try: os.symlink(source, dest) - except Exception, e: - print e, "--", source, "->", dest + except Exception as e: + print(e, "--", source, "->", dest) + sys.exit(1) + +def sanitize_filename(filename): + unsafe = re.compile("[^a-zA-Z0-9\-_ ,\\.]+") + return unsafe.sub("", filename) + +if __name__ == "__main__": + try: + destdir = sys.argv[1] + except: + print("Usage: %s destdir" % sys.argv[0]) sys.exit(1) + + main(destdir)