text: import urwid
  import os
  import json
  from pathlib import Path
  from tempfile import gettempdir
  from operator import itemgetter
  from subprocess import run, PIPE, STDOUT
  
  from qbittorrentapi import Client
  from qbittorrentapi import APIError
  
  #import logging
  #logging.basicConfig(level=logging.INFO)
  # logging.disable(level=logging.CRITICAL)
  # logging.basicConfig(level=logging.WARNING,
  #                    format='[%(asctime)s] {%(name)s:%(lineno)d} %(levelname)s - %(message)s',
  #                    filename='/home/user/output.txt',
  #                    filemode='w')
  
  
  def quit_tormon(*args, **kwargs):
      raise urwid.ExitMainLoop()
  
  
  class Tormon(object):
      def __init__(self):
          super().__init__()
          with open(Path(__file__).parent / 'qbittorrent_config.txt') as file:
              qbt_config = json.load(file)
          self.qbt_client = Client(**qbt_config)
          self.log_path = Path(gettempdir()) / 'znp_logs/'
  
          self.free_space_w = urwid.Filler(urwid.Text(''))
          self.torrents_stats_w = urwid.Text('')
          self.transiting_torrents_column1 = urwid.Text('', wrap=urwid.CLIP)
          self.transiting_torrents_column2 = urwid.Text('', align=urwid.RIGHT, wrap=urwid.CLIP)
          self.footer_w = urwid.Filler(urwid.Text('', align=urwid.RIGHT, wrap=urwid.CLIP))
  
      @staticmethod
      def handle_key(key):
          if key in ('q', 'Q'):
              quit_tormon()
  
      def refresh(self, loop=None, user_data=None):
          self.free_space_w.base_widget.set_text(print_free_space())
          self.torrents_stats_w.base_widget.set_text(print_torrent_stats(self.qbt_client))
          transiting_torrents_tuple = print_transiting_torrents(self.log_path, 10000)
          self.transiting_torrents_column1.base_widget.set_text(transiting_torrents_tuple[0])
          self.transiting_torrents_column2.base_widget.set_text(transiting_torrents_tuple[1])
          self.footer_w.base_widget.set_text(print_qbt_version(self.qbt_client))
          loop.set_alarm_in(sec=2, callback=self.refresh)
  
      def run(self):
          transiting_torrents_w = urwid.Columns(
              widget_list=[self.transiting_torrents_column1,
                           self.transiting_torrents_column2],
              dividechars=2
          )
  
          mainframe = urwid.Pile(
              [
                  ('fixed', 2, self.free_space_w),
                  ('fixed', 1, urwid.Filler(urwid.Divider())),
                  ('fixed', 1, urwid.Filler(urwid.Text(" >>> Torrent Stats <<<"))),
                  ('pack', self.torrents_stats_w),
                  ('fixed', 1, urwid.Filler(urwid.Divider())),
                  ('fixed', 1, urwid.Filler(urwid.Text(" >>> Torrent Handling <<<"))),
                  ('pack', transiting_torrents_w),
                  urwid.Filler(urwid.Divider()),
                  ('fixed', 1, self.footer_w)
              ]
          )
  
          loop = urwid.MainLoop(
              mainframe,
              handle_mouse=False,
              unhandled_input=self.handle_key
          )
          loop.set_alarm_in(sec=.01, callback=self.refresh)
          loop.run()
  
  
  def print_free_space():
      output = []
      df_cmd = '/bin/df -h | grep -E "/dev/vda1|Filesystem"'
      result = run([df_cmd], shell=True, stdout=PIPE, stderr=STDOUT, encoding='utf-8')
      for line in [line for line in result.stdout.split('\n') if line != ""]:
          output.append(line)
      return '\n'.join(output)
  
  
  def print_torrent_stats(qbt):
      output = []
      try:
          torrent_list = qbt.torrents_info(status_filter='all')
          stats = {}
          stats.setdefault('category', {})
          stats.setdefault('state', {})
  
          state_map = {'pausedUP': "Completed",
                       'uploading': 'Uploading',
                       'stalledUP': 'Uploading',
                       'forcedUP': 'Uploading',
                       'queuedUP': 'Queued',
                       'pausedDL': "Paused",
                       'downloading': 'Downloading',
                       'stalledDL': "Downloading",
                       'checkingUP': "Checking",
                       'checkingDL': "Checking",
                       "metaDL": "Metadata Dl"}
  
          # get counts of categories and states
          for torrent in torrent_list:
              cat = torrent.category
              if cat != "":
                  stats['category'].update({cat: stats['category'].setdefault(cat, 0)+1})
              state = torrent.state
              if state in state_map:  # map qbittorrent state to local states
                  state = state_map[state]
              stats['state'].update({state: stats['state'].setdefault(state, 0)+1})
  
          # find longest length of torrents' states
          max_key_length = 0
          if stats['state']:
              max_key_length = max(map(len, stats['state']))
  
          # generate printable strings of counts
          for _ in range(max(len(stats['state']), len(stats['category']))):
              try:
                  (state_name, state_count) = stats['state'].popitem()
              except KeyError:
                  (state_name, state_count) = ("", 0)
              try:
                  (category_name, category_count) = stats['category'].popitem()
              except KeyError:
                  (category_name, category_count) = ("", 0)
  
              string = ""
              if state_name != "":
                  string = "%s:%s%2d" % (state_name, ' '*(max_key_length-len(state_name)+1), state_count)
              if category_name != "":
                  space_count = (max_key_length+10) - len(string)
                  string += "%s%s: %s" % (' '*space_count, category_name, category_count)
              output.append(string)
          return '\n'.join(output)
  
      except APIError as e:
          output.append("Error: %s" % e)
          return '\n'.join(output)
  
  
  def print_transiting_torrents(log_path, cols):
      torrent_list = []
      filename_list = []
      transfer_details_list = []
  
      for directory, _, files in os.walk(log_path):
          # process each debug log file
          for f in [open(Path(directory, file)) for file in files if "_debug.log" in file]:
              lines = f.readlines()
              srch_list = ['INFO - >>> Starting transit for ', '>>> Starting identification for ', ]
              for srch in srch_list:
                  try:
                      # extract filename from last line in log that actually contains a filename
                      # example line: [2019-04-27 17:37:08,453] {       processor: 75} INFO - Filename: American.Gods.S01E01.720p.WEBRip.X264-DEFLATE.mkv
                      filename = [line[line.find(srch)+len(srch):].strip() for line in lines if srch in line][-1]
                      if filename:
                          if filename.endswith('...'):
                              filename = filename[:-3]
                          break
                  except IndexError:  # failure here would suggest there are no filename in the file...
                      filename = ''
              try:
                  lastline = lines[-1]  # there should always be a last line...even for an empty file...but not always
              except IndexError:
                  lastline = ''
  
              srch = 'Output:'          # indication of rsync output
              cushion = 15              # chars between filename and details of last line of log file
              filename_end_length = 3   # if trimming filename, how much to keep from end of filename (e.g. 3 for file extension)
              cushion_char = "_"        # char to use to separate filename and list line of log detail
              percent_uploaded = 0      # initialize percent uploaded of file to zero
  
              # extract relevant portion of lastline of log file
              if srch in lastline:   # if rsync output is in lastline
                  transfer_details = lastline[lastline.find(srch)+len(srch):].strip()  # get rsync output e.g "1.64G  97%  170.30kB/s    0:04:02"
                  if transfer_details.find('%') >= 0:
                      try:
                          percent_uploaded = int(transfer_details[transfer_details.find('%')-2:transfer_details.find('%')])
                      except ValueError:  # somehow a non-integer was found...
                          percent_uploaded = 0
              else:   # if something other than rsync output is lastline
                  # example: [2019-04-27 20:10:28,305] {      processor:232} INFO - >>> Finished Database Query
                  transfer_details = '{' + lastline[lastline.find('{')+1:cols-15].strip()
              transfer_details_list.append(transfer_details)
              transfer_details_len = len(transfer_details)
  
              # cut out a middle piece of filename if necessary due to screen width
              filename_list.append(filename)
              if len(filename) > (cols - transfer_details_len - cushion):
                  filename = filename[:cols - transfer_details_len - cushion - 3 - filename_end_length] + '...' + filename[len(filename)-filename_end_length:]
  
              # build list of tuples (torrent, % uploaded)
              torrent_list.append(("%s%s%s" % (filename, cushion_char*(cols-len(filename)-transfer_details_len), transfer_details), percent_uploaded, filename, transfer_details))
  
      # sort torrents by percent uploaded and add to be printed
      torrent_list.sort(key=itemgetter(1), reverse=True)
  
      return '\n'.join([x[2] for x in torrent_list]), '\n'.join([x[3] for x in torrent_list])
  
  
  def print_qbt_version(qbt):
      ret = ''
      try:
          version = qbt.app.version
          api_version = qbt.app.web_api_version
      except APIError:
          pass
      else:
          ret = f'{version} ({api_version})'
      return ret
  
  
  if __name__ == '__main__':
      Tormon().run()
  