diff options
Diffstat (limited to 'gallery_dl/output.py')
| -rw-r--r-- | gallery_dl/output.py | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/gallery_dl/output.py b/gallery_dl/output.py new file mode 100644 index 0000000..327b69a --- /dev/null +++ b/gallery_dl/output.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015-2019 Mike Fährmann +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. + +import os +import sys +import shutil +import logging +from . import config, util + + +# -------------------------------------------------------------------- +# Logging + +LOG_FORMAT = "[{name}][{levelname}] {message}" +LOG_FORMAT_DATE = "%Y-%m-%d %H:%M:%S" +LOG_LEVEL = logging.INFO + + +class Logger(logging.Logger): + """Custom logger that includes extractor and job info in log records""" + extractor = util.NONE + job = util.NONE + + def makeRecord(self, name, level, fn, lno, msg, args, exc_info, + func=None, extra=None, sinfo=None, + factory=logging._logRecordFactory): + rv = factory(name, level, fn, lno, msg, args, exc_info, func, sinfo) + rv.extractor = self.extractor + rv.job = self.job + return rv + + +def initialize_logging(loglevel): + """Setup basic logging functionality before configfiles have been loaded""" + # convert levelnames to lowercase + for level in (10, 20, 30, 40, 50): + name = logging.getLevelName(level) + logging.addLevelName(level, name.lower()) + + # register custom Logging class + logging.Logger.manager.setLoggerClass(Logger) + + # setup basic logging to stderr + formatter = logging.Formatter(LOG_FORMAT, LOG_FORMAT_DATE, "{") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + handler.setLevel(loglevel) + root = logging.getLogger() + root.setLevel(logging.NOTSET) + root.addHandler(handler) + + return logging.getLogger("gallery-dl") + + +def setup_logging_handler(key, fmt=LOG_FORMAT, lvl=LOG_LEVEL): + """Setup a new logging handler""" + opts = config.interpolate(("output", key)) + if not opts: + return None + if not isinstance(opts, dict): + opts = {"path": opts} + + path = opts.get("path") + mode = opts.get("mode", "w") + encoding = opts.get("encoding", "utf-8") + try: + path = util.expand_path(path) + handler = logging.FileHandler(path, mode, encoding) + except (OSError, ValueError) as exc: + logging.getLogger("gallery-dl").warning( + "%s: %s", key, exc) + return None + except TypeError as exc: + logging.getLogger("gallery-dl").warning( + "%s: missing or invalid path (%s)", key, exc) + return None + + level = opts.get("level", lvl) + logfmt = opts.get("format", fmt) + datefmt = opts.get("format-date", LOG_FORMAT_DATE) + formatter = logging.Formatter(logfmt, datefmt, "{") + handler.setFormatter(formatter) + handler.setLevel(level) + + return handler + + +def configure_logging_handler(key, handler): + """Configure a logging handler""" + opts = config.interpolate(("output", key)) + if not opts: + return + if isinstance(opts, str): + opts = {"format": opts} + if handler.level == LOG_LEVEL and "level" in opts: + handler.setLevel(opts["level"]) + if "format" in opts or "format-date" in opts: + logfmt = opts.get("format", LOG_FORMAT) + datefmt = opts.get("format-date", LOG_FORMAT_DATE) + formatter = logging.Formatter(logfmt, datefmt, "{") + handler.setFormatter(formatter) + + +# -------------------------------------------------------------------- +# Utility functions + +def replace_std_streams(errors="replace"): + """Replace standard streams and set their error handlers to 'errors'""" + for name in ("stdout", "stdin", "stderr"): + stream = getattr(sys, name) + setattr(sys, name, stream.__class__( + stream.buffer, + errors=errors, + newline=stream.newlines, + line_buffering=stream.line_buffering, + )) + + +# -------------------------------------------------------------------- +# Downloader output + +def select(): + """Automatically select a suitable output class""" + pdict = { + "default": PipeOutput, + "pipe": PipeOutput, + "term": TerminalOutput, + "terminal": TerminalOutput, + "color": ColorOutput, + "null": NullOutput, + } + omode = config.get(("output", "mode"), "auto").lower() + if omode in pdict: + return pdict[omode]() + elif omode == "auto": + if hasattr(sys.stdout, "isatty") and sys.stdout.isatty(): + return ColorOutput() if ANSI else TerminalOutput() + else: + return PipeOutput() + else: + raise Exception("invalid output mode: " + omode) + + +class NullOutput(): + + def start(self, path): + """Print a message indicating the start of a download""" + + def skip(self, path): + """Print a message indicating that a download has been skipped""" + + def success(self, path, tries): + """Print a message indicating the completion of a download""" + + +class PipeOutput(NullOutput): + + def skip(self, path): + print(CHAR_SKIP, path, sep="", flush=True) + + def success(self, path, tries): + print(path, flush=True) + + +class TerminalOutput(NullOutput): + + def __init__(self): + self.short = config.get(("output", "shorten"), True) + if self.short: + self.width = shutil.get_terminal_size().columns - OFFSET + + def start(self, path): + print(self.shorten(" " + path), end="", flush=True) + + def skip(self, path): + print(self.shorten(CHAR_SKIP + path)) + + def success(self, path, tries): + print("\r", self.shorten(CHAR_SUCCESS + path), sep="") + + def shorten(self, txt): + """Reduce the length of 'txt' to the width of the terminal""" + if self.short and len(txt) > self.width: + hwidth = self.width // 2 - OFFSET + return "".join(( + txt[:hwidth-1], + CHAR_ELLIPSIES, + txt[-hwidth-(self.width % 2):] + )) + return txt + + +class ColorOutput(TerminalOutput): + + def start(self, path): + print(self.shorten(path), end="", flush=True) + + def skip(self, path): + print("\033[2m", self.shorten(path), "\033[0m", sep="") + + def success(self, path, tries): + print("\r\033[1;32m", self.shorten(path), "\033[0m", sep="") + + +if os.name == "nt": + ANSI = os.environ.get("TERM") == "ANSI" + OFFSET = 1 + CHAR_SKIP = "# " + CHAR_SUCCESS = "* " + CHAR_ELLIPSIES = "..." +else: + ANSI = True + OFFSET = 0 + CHAR_SKIP = "# " + CHAR_SUCCESS = "✔ " + CHAR_ELLIPSIES = "…" |
