diff --git a/.gitignore b/.gitignore index 1d17dae..93e81c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .venv +*~ diff --git a/tivomirror.py b/tivomirror.py index cc75ef3..9bb6472 100755 --- a/tivomirror.py +++ b/tivomirror.py @@ -13,6 +13,7 @@ import http.cookiejar import datetime import getopt import errno +import fcntl import functools import logging import logging.handlers @@ -30,11 +31,13 @@ import urllib.request, urllib.error, urllib.parse import xml.dom.minidom import yaml +from contextlib import contextmanager from io import TextIOWrapper class Config: config = '~/.tivo/config.yaml' + lockfile = config + '.lock' cookies = "cookies.txt" gig = 1024.0 * 1024 * 1024 headers = requests.utils.default_headers() @@ -382,6 +385,16 @@ class FdLogger(threading.Thread): self.logger.exception("") +@contextmanager +def exclusive(): + with open(os.path.expanduser(config.lockfile), 'w') as f: + try: + fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError as e: + raise TivoException('another tivomirror instance is already running') + yield 'locked' + fcntl.lockf(f, fcntl.LOCK_UN) + @timeout(43200) def download_item(item, mak, target): global config @@ -523,47 +536,50 @@ def mirror(toc, downloaddb, one=False): logger.error("{}: {:.1f} GB available, at least {:.1f} GB needed, stopping".format\ (config.targetdir, avail / config.gig, config.minfree / config.gig)) sys.exit(1) - - items = toc.getItems() - logger.info("*** {} shows listed".format(len(items))) - for item in items: - options = wantitem(item, downloaddb) - if isinstance(options, str): - logger.debug("*** skipping \"{}\": {}".format(item.name, options)) - else: - download_one(item, downloaddb, options) - if one: - break + + with exclusive() as lock: + items = toc.getItems() + logger.info("*** {} shows listed".format(len(items))) + for item in items: + options = wantitem(item, downloaddb) + if isinstance(options, str): + logger.debug("*** skipping \"{}\": {}".format(item.name, options)) + else: + download_one(item, downloaddb, options) + if one: + break def download_episode(toc, downloaddb, episode): - items = toc.getItems() - options = {} - for item in items: - if item.title == episode or item.name == episode or item.episode == episode: - for i in (item.title, item.episode, item.name): - if i in IncludeShow.includes: - options = IncludeShow.includes[i] - download_one(item, downloaddb, options) - return + with exclusive() as lock: + items = toc.getItems() + options = {} + for item in items: + if item.title == episode or item.name == episode or item.episode == episode: + for i in (item.title, item.episode, item.name): + if i in IncludeShow.includes: + options = IncludeShow.includes[i] + download_one(item, downloaddb, options) + return def printtoc(toc, downloaddb): - items = toc.getItems() - print("*** {} shows listed".format(len(items))) - shows = {} - for item in items: - if item.title not in shows: - shows[item.title] = [] - shows[item.title].append(item) - for title in sorted(shows): - for item in sorted(shows[title], key=lambda i: i.name): - options = wantitem(item, downloaddb) - if isinstance(options, str): - print("{:>7.7s}: {}".format(options, item.name)) - continue - print("*** downloading {} ({:.3f} GB)".format(item.name, item.sourcesize / 1e9)) - print("*** {} shows listed".format(len(items))) + with exclusive() as lock: + items = toc.getItems() + print("*** {} shows listed".format(len(items))) + shows = {} + for item in items: + if item.title not in shows: + shows[item.title] = [] + shows[item.title].append(item) + for title in sorted(shows): + for item in sorted(shows[title], key=lambda i: i.name): + options = wantitem(item, downloaddb) + if isinstance(options, str): + print("{:>7.7s}: {}".format(options, item.name)) + continue + print("*** downloading {} ({:.3f} GB)".format(item.name, item.sourcesize / 1e9)) + print("*** {} shows listed".format(len(items))) def usage():