text: #!/usr/bin/env python
  # Weston Nielson <wnielson@github>
  #
  
  import filecmp
  import getpass
  import json
  import logging
  import logging.config
  import multiprocessing
  import os
  import pipes
  import re
  import shlex
  import shutil
  import subprocess
  import sys
  import time
  import urllib
  import urllib2
  import uuid
  
  from distutils.spawn import find_executable
  
  try:
      from xml.etree import cElementTree as ET
  except:
      from xml.etree import ElementTree as ET
  
  import psutil
  
  try:
      from termcolor import colored
  except:
      def colored(msg, *args):
          return msg
  
  log = logging.getLogger("prt")
  
  if sys.platform == "darwin":
      # OS X
      TRANSCODER_DIR = "/Applications/Plex Media Server.app/Contents/"
      SETTINGS_PATH  = "~/Library/Preferences/com.plexapp.plexmediaserver"
  elif sys.platform.startswith('linux'):
      # Linux
      TRANSCODER_DIR = "/usr/lib/plexmediaserver/"
      SETTINGS_PATH  = "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Preferences.xml"
  else:
      raise NotImplementedError("This platform is not yet supported")
  
  
  def printf(message, *args, **kwargs):
      color = kwargs.get('color')
      attrs = kwargs.get('attrs')
      sys.stdout.write(colored(message % args, color, attrs=attrs))
      sys.stdout.flush()
  
  
  def get_auth_token():
      try:
          # Look for token in plexmediaserver SETTINGS_PATH file
          prefs = ET.parse(SETTINGS_PATH).getroot()
          prefs_token = prefs.attrib['PlexOnlineToken']
  
      except KeyError, e:
          print "Couldn't find PlexOnlineToken in settings file - %s." % SETTINGS_PATH
          print "Obtaining Plex auth token from plex.tv..."
  
          # Fallback to prompting for Plex username/password
          url = "https://plex.tv/users/sign_in.json"
          payload = urllib.urlencode({
              "user[login]": raw_input("Plex Username: "),
              "user[password]": getpass.getpass("Plex Password: "),
              "X-Plex-Client-Identifier": "Plex-Remote-Transcoder-v%s" % __version__,
              "X-Plex-Product": "Plex-Remote-Transcoder",
              "X-Plex-Version": __version__
          })
          req = urllib2.Request(url, payload)
  
          try:
              res = urllib2.urlopen(req)
          except:
              print "Error getting auth token...invalid credentials?"
              return False
  
          if res.code not in [200, 201]:
              print "Invalid credentials"
              return False
  
          data = json.load(res)
          print "Using Plex auth token obtained from plex.tv"
          return data['user']['authToken']
  
      except Exception, e:
          printf("ERROR: Couldn't open settings file - %s", SETTINGS_PATH, color="red")
          return False
  
      print "Using Plex auth token from settings file - %s." % SETTINGS_PATH
      return prefs_token
  
  
  DEFAULT_CONFIG = {
      "ipaddress": "",
      "path_script":    None,
      "servers_script": "/usr/sbin/cluster-manager",
      "servers":   {},
      "auth_token": None,
      "logging":   {
          "version": 1,
          "disable_existing_loggers": False,
          "formatters": {
              "simple": {
                  "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
              }
          },
          "handlers": {
              "file_handler": {
                  "class": "logging.handlers.RotatingFileHandler",
                  "level": "INFO",
                  "formatter": "simple",
                  "filename": "/opt/plex/tmp/prt.log",
                  "maxBytes": 10485760,
                  "backupCount": 20,
                  "encoding": "utf8"
              },
          },
          "loggers": {
              "prt": {
                  "level": "DEBUG",
                  "handlers": ["file_handler"],
                  "propagate": "no"
              }
          }
      }
  }
  
  # This is the name we give to the original transcoder, which must be renamed
  NEW_TRANSCODER_NAME      = "plex_transcoder"
  ORIGINAL_TRANSCODER_NAME = "Plex Transcoder"
  
  REMOTE_ARGS = ("%(env)s;"
                 "cd %(working_dir)s;"
                 "%(command)s %(args)s")
  
  LOAD_AVG_RE = re.compile(r"load averages: ([\d\.]+) ([\d\.]+) ([\d\.]+)")
  
  PRT_ID_RE   = re.compile(r'PRT_ID=([0-9a-f]{32})', re.I)
  SESSION_RE  = re.compile(r'/session/([^/]*)/')
  SSH_HOST_RE = re.compile(r'ssh +([^@]+)@([^ ]+)')
  
  __author__  = "Weston Nielson <wnielson@github>"
  __version__ = "0.4.4"
  
  def get_config():
      path = os.path.expanduser("~/.prt.conf")
      try:
          return json.load(open(path))
      except Exception, e:
          return DEFAULT_CONFIG.copy()
  
  
  def save_config(d):
      path = os.path.expanduser("~/.prt.conf")
      try:
          json.dump(d, open(path, 'w'), indent=4)
          return True
      except Exception, e:
          print "Error loading config: %s" % str(e)
      return False
  
  
  def get_system_load_local():
      """
      Returns a list of float representing the percentage load of this machine.
      """
      nproc = multiprocessing.cpu_count()
      load  = os.getloadavg()
      return [l/nproc * 100 for l in load]
  
  
  def get_system_load_remote(host, port, user):
      """
      Gets the result from ``get_system_load_local`` of a remote machine.
      """
      proc = subprocess.Popen(["ssh", "%s@%s" % (user, host), "-p", port, "prt", "get_load"], stdout=subprocess.PIPE)
      proc.wait()
      return [float(i) for i in proc.stdout.read().strip().split()]
  
  
  def setup_logging():
      config = get_config()
      logging.config.dictConfig(config["logging"])
  
  
  def get_transcoder_path(name=NEW_TRANSCODER_NAME):
      """
      Returns the full path to ``name`` located in ``TRANSCODER_DIR``.
      """
      return os.path.join(TRANSCODER_DIR, name)
  
  
  def rename_transcoder():
      """
      Moves the original transcoder "Plex Transcoder" to the new name given
      by ``TRANSCODER_NAME``.
      """
      old_path = get_transcoder_path(ORIGINAL_TRANSCODER_NAME)
      new_path = get_transcoder_path(NEW_TRANSCODER_NAME)
  
      if os.path.exists(new_path):
          print "Transcoder appears to have been renamed previously...not renaming (try overwrite option)"
          return False
  
      try:
          os.rename(old_path, new_path)
      except Exception, e:
          print "Error renaming original transcoder: %s" % str(e)
          return False
  
      return True
  
  
  def install_transcoder():
      prt_remote = find_executable("prt_remote")
      if not prt_remote:
          print "Couldn't find `prt_remote` executable"
          return
  
      print "Renaming original transcoder"
      if rename_transcoder():
          try:
              shutil.copyfile(prt_remote, get_transcoder_path(ORIGINAL_TRANSCODER_NAME))
              os.chmod(get_transcoder_path(ORIGINAL_TRANSCODER_NAME), 0755)
          except Exception, e:
              print "Error installing new transcoder: %s" % str(e)
  
  
  # Overwrite_transcoder_after_upgrade function
  def overwrite_transcoder_after_upgrade():
      """
      Moves the upgraded transcoder "Plex Transcoder" to the new name given
      by ``TRANSCODER_NAME`` if the plex package has overwritten the old one.
      """
      old_path = get_transcoder_path(ORIGINAL_TRANSCODER_NAME)
      new_path = get_transcoder_path(NEW_TRANSCODER_NAME)
  
      prt_remote = find_executable("prt_remote")
      if not prt_remote:
          print "Couldn't find `prt_remote` executable"
          sys.exit(2)
      elif os.path.exists(new_path):
             print "Transcoder appears to have been renamed previously...checking if it's been overwritten"
             if not filecmp.cmp(prt_remote, get_transcoder_path(ORIGINAL_TRANSCODER_NAME), shallow=1):
                 try:
                     shutil.copyfile(prt_remote, get_transcoder_path(ORIGINAL_TRANSCODER_NAME))
                     os.chmod(get_transcoder_path(ORIGINAL_TRANSCODER_NAME), 0755)
                 except Exception, e:
                     print "Error installing new transcoder: %s" % str(e)
                     sys.exit(2)
             else:
                 print "Transcoder hasn't been overwritten by upgrade, nothing to do"
                 sys.exit(1)
      else:
           print "Transcoder hasn't been previously installed, please use install option"
           sys.exit(1)
  
  def build_env(host=None):
      # TODO: This really should be done in a way that is specific to the target
      #       in the case that the target is a different architecture than the host
      ffmpeg_path = os.environ.get("FFMPEG_EXTERNAL_LIBS", "")
      backslashcheck = re.search(r'\\', ffmpeg_path)
      if backslashcheck is not None:
          ffmpeg_path_fixed = ffmpeg_path.replace('\\','')
          os.environ["FFMPEG_EXTERNAL_LIBS"] = str(ffmpeg_path_fixed)
  
      envs = ["export %s=%s" % (k, pipes.quote(v)) for k,v in os.environ.items()]
      envs.append("export PRT_ID=%s" % uuid.uuid1().hex)
      return ";".join(envs)
  
  
  # def check_gracenote_tmp():
  
  
  
  def transcode_local():
      setup_logging()
  
      # The transcoder needs to have the propery LD_LIBRARY_PATH
      # set, otherwise it cannot run
      #os.environ["LD_LIBRARY_PATH"] = "%s:$LD_LIBRARY_PATH" % LD_LIBRARY_PATH
      #for k, v in ENV_VARS.items():
      #    os.environ[k] = v
  
      config = get_config()
      is_debug = config['logging']['loggers']['prt']['level'] == 'DEBUG'
  
      if is_debug:
          log.info('Debug mode - enabling verbose ffmpeg output')
  
          # Change logging mode for FFMpeg to be verbose
          for i, arg in enumerate(sys.argv):
              if arg == '-loglevel':
                  sys.argv[i+1] = 'verbose'
              elif arg == '-loglevel_plex':
                  sys.argv[i+1] = 'verbose'
  
      # Set up the arguments
      args = [get_transcoder_path()] + sys.argv[1:]
  
      log.info("Launching transcode_local: %s\n" % args)
  
      # Spawn the process
      proc = subprocess.Popen(args, stderr=subprocess.PIPE)
  
      while True:
          output = proc.stderr.readline()
          if output == '' and proc.poll() is not None:
              break
          if output and is_debug:
              log.debug(output.strip('\n'))
  
  def transcode_remote():
      setup_logging()
  
      log.info("Checking for orphaned PRT processes")
      found = 0
      for proc in psutil.process_iter():
          try:
              if proc.name == "ssh" and 'PLEX_MEDIA_SERVER' in ' '.join(proc.cmdline):
                  if proc.parent.pid == 1:
                      log.info('Found orphaned PRT process (pid %s)...killing' % proc.pid)
                      found += 1
                      proc.terminate()
                      proc.wait()
          except psutil.NoSuchProcess:
              pass
  
      log.info("Found %d orphaned PRT processes" % found)
  
      config = get_config()
      args   = sys.argv[1:]
  
      if 'eae_prefix' in ' '.join(args):
          log.info("Found EAE is being used...forcing local transcode")
          return transcode_local()
  
      # Check to see if we need to call a user-script to replace/modify the file path
      if config.get("path_script", None):
          idx = 0
          # The file path comes after the "-i" command line argument
          for i, v in enumerate(args):
              if v == "-i":
                  idx = i+1
                  break
  
          # Found the requested video path
          path = args[idx]
  
          try:
              proc = subprocess.Popen([config.get("path_script"), path], stdout=subprocess.PIPE)
              proc.wait()
              new_path = proc.stdout.readline().strip()
              if new_path:
                  log.debug("Replacing path with: %s" % new_path)
                  args[idx] = new_path
          except Exception, e:
              log.error("Error calling path_script: %s" % str(e))
  
      command = REMOTE_ARGS % {
          "env":          build_env(),
          "working_dir":  pipes.quote(os.getcwd()),
          "command":      "prt_local",
          "args":         ' '.join([pipes.quote(a) for a in args])
      }
  
      servers = config["servers"]
  
      # Look to see if we need to run an external script to get hosts
      if config.get("servers_script", None):
          try:
              proc = subprocess.Popen(config["servers_script"].split(), stdout=subprocess.PIPE)
              proc.wait()
  
              servers = {}
              for line in proc.stdout.readlines():
                  hostname, port, user = line.strip().split()
                  servers[hostname] = {
                      "port": port,
                      "user": user
                  }
          except Exception, e:
              log.error("Error retreiving host list via '%s': %s" % (config["servers_script"], str(e)))
  
      hostname, host = None, None
  
      # Let's try to load-balance
      min_load = None
      for hostname, host in servers.items():
  
          log.debug("Getting load for host '%s'" % hostname)
          load = get_system_load_remote(hostname, host["port"], host["user"])
  
          if not load:
              # If no load is returned, then it is likely that the host
              # is offline or unreachable
              log.debug("Couldn't get load for host '%s'" % hostname)
              continue
  
          log.debug("Log for '%s': %s" % (hostname, str(load)))
  
          # XXX: Use more that just 1-minute load?
          if min_load is None or min_load[1] > load[0]:
              min_load = (hostname, load[0],)
  
      if min_load is None:
          log.info("No hosts found...using local")
          return transcode_local()
  
      # Select lowest-load host
      log.info("Host with minimum load is '%s'" % min_load[0])
      hostname, host = min_load[0], servers[min_load[0]]
  
      log.info("Using transcode host '%s'" % hostname)
  
      # Remap the 127.0.0.1 reference to the proper address
      #command = command.replace("127.0.0.1", config["ipaddress"])
  
      #
      # TODO: Remap file-path to PMS URLs
      #
  
      args = ["ssh", "-tt", "-R", "32400:127.0.0.1:32400", "%s@%s" % (host["user"], hostname), "-p", host["port"]] + [command]
  
  
      log.info("Launching transcode_remote with args %s\n" % args)
  
      # Spawn the process
      proc = subprocess.Popen(args)
      proc.wait()
  
      log.info("Transcode stopped on host '%s'" % hostname)
  
  
  def re_get(regex, string, group=0, default=None):
      match = regex.search(string)
      if match:
          try:
              return match.groups()[group]
          except:
              if group == "all":
                  return match.groups()
      return default
  
  def et_get(node, attrib, default=None):
      if node is not None:
          return node.attrib.get(attrib, default)
      return default
  
  
  def get_plex_sessions(auth_token=None):
      url = 'http://localhost:32400/status/sessions'
      if auth_token:
          url += "?X-Plex-Token=%s" % auth_token
  
      res = urllib.urlopen(url)
      dom = ET.parse(res)
      sessions = {}
      for node in dom.findall('.//Video'):
          session_id = et_get(node.find('.//TranscodeSession'), 'key')
          if session_id:
              sessions[session_id] = {
                  'file': et_get(node.find('.//Media/Part'), 'file')
          }
      return sessions
  
  def get_sessions():
      sessions = {}
  
      config = get_config()
      if config.get('auth_token') == None:
          config['auth_token'] = get_auth_token()
          if not config['auth_token']:
              return sessions
          save_config(config)
  
      sessions = {}
  
      plex_sessions = get_plex_sessions(auth_token=config['auth_token'])
      for proc in psutil.process_iter():
          parent_name = None
          try:
              if callable(proc.parent):
                  parent_name = proc.parent().name()
              else:
                  parent_name = proc.parent.name
          except:
              continue
  
          if not parent_name:
              continue
  
          pinfo = proc.as_dict(['name', 'cmdline'])
  
          # Check the parent to make sure it is the "Plex Transcoder"
          if pinfo['name'] == 'ssh' and 'plex' in parent_name.lower():
              cmdline = ' '.join(pinfo['cmdline'])
              m = PRT_ID_RE.search(cmdline)
              if m:
                  session_id = re_get(SESSION_RE, cmdline)
                  data = {
                      'proc': pinfo,
                      'plex': plex_sessions.get(session_id, {}),
                      'host': {}
                  }
  
                  host = re_get(SSH_HOST_RE, cmdline, 'all')
                  if host:
                      data['host'] = {
                          'user':    host[0],
                          'address': host[1]
                      }
  
                  sessions[m.groups()[0]] = data
      return sessions
  
  def check_config():
      """
      Run through various diagnostic checks to see if things are configured
      correctly.
      """
      config = get_config()
      errors = []
  
      printf("Performing PRT configuration check\n\n", color="blue", attrs=['bold'])
  
      # First, check the user
      user = getpass.getuser()
      if user != "plex":
          printf("WARNING: Current user is not 'plex'\n", color="red")
  
      try:
          settings_fh = open(SETTINGS_PATH)
          dom = ET.parse(settings_fh)
          settings = dom.getroot().attrib
      except Exception, e:
          printf("ERROR: Couldn't open settings file - %s", SETTINGS_PATH, color="red")
          return False
  
      config = get_config()
      if config.get('auth_token') == None:
          config['auth_token'] = get_auth_token()
  
      url = 'http://localhost:32400/library/sections'
      if config['auth_token']:
          url += "?X-Plex-Token=%s" % config['auth_token']
  
      res = urllib.urlopen(url)
      dom = ET.parse(res)
      media_paths = []
      for node in dom.findall('.//Location'):
          path = et_get(node, 'path')
          if path not in media_paths:
              media_paths.append(path)
  
      media_paths.append(TRANSCODER_DIR)
      paths_modes = {
          5: media_paths,
          7: [settings['TranscoderTempDirectory']]
      }
  
      servers = config["servers"]
  
      # Look to see if we need to run an external script to get hosts
      if config.get("servers_script", None):
          try:
              proc = subprocess.Popen(config["servers_script"].split(), stdout=subprocess.PIPE)
              proc.wait()
  
              servers = {}
              for line in proc.stdout.readlines():
                  hostname, port, user = line.strip().split()
                  servers[hostname] = {
                      "port": port,
                      "user": user
                  }
          except Exception, e:
              log.error("Error retreiving host list via '%s': %s" % (config["servers_script"], str(e)))
              servers = config["servers"]
  
      # Let's check SSH access
      for address, server in servers.items():
          printf("Host %s\n", address)
  
          proc = subprocess.Popen(["ssh", "%s@%s" % (server["user"], address),
              "-p", server["port"], "prt", "get_load"],
              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          proc.wait()
  
          printf("  Connect: ")
          if proc.returncode != 0:
              printf("FAIL\n", color="red")
              printf("    %s\n" % proc.stderr.read())
              continue
          else:
              printf("OK\n", color="green")
  
          for req_mode, paths in paths_modes.items():
              for path in paths:
                  printf("  Path: '%s'\n", path)
                  proc = subprocess.Popen(["ssh", "%s@%s" % (server["user"], address),
                      "-p", server["port"], "stat", "--printf='%U %a'", pipes.quote(path)],
                      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                  proc.wait()
  
                  username, mode = proc.stdout.read().strip().split()
                  printf("    User:  %s\n", username)
                  printf("    Mode:  %s\n", mode)
  
                  if username != 'plex':
                      printf("    WARN:  Not owned by plex user\n", color="yellow")
                      if int(mode[-1]) < req_mode:
                          printf("    ERROR: Bad permissions\n", color="red")
                  else:
                      if int(mode[0]) < req_mode:
                          printf("    ERROR: Bad permissions\n", color="red")
  
          printf("\n")
  
  
  def sessions():
      if psutil is None:
          print "Missing required library 'psutil'.  Try 'pip install psutil'."
          return
  
      sessions = get_sessions()
      for i, (session_id, session) in enumerate(sessions.items()):
          print "Session %s/%s" % (i+1, len(sessions))
          print "  Host: %s" % session.get('host', {}).get('address')
          print "  File: %s" % session.get('plex', {}).get('file')
  
  
  def version():
      print "Plex Remote Transcoder version %s, Copyright (C) %s\n" % (__version__, __author__)
  
  
  # Usage function
  def usage():
      version()
      print "Plex Remote Transcode comes with ABSOLUTELY NO WARRANTY.\n\n"\
            "This is free software, and you are welcome to redistribute it and/or modify\n"\
            "it under the terms of the MIT License.\n\n"
      print "Usage:\n"
      print "  %s [options]\n" % os.path.basename(sys.argv[0])
      print (
          "Options:\n\n" 
          "  usage, help, -h, ?    Show usage page\n" 
          "  get_load              Show the load of the system\n" 
          "  get_cluster_load      Show the load of all systems in the cluster\n" 
          "  install               Install PRT for the first time and then sets up configuration\n" 
          "  overwrite             Fix PRT after PMS has had a version update breaking PRT\n" 
          "  add_host              Add an extra host to the list of slaves PRT is to use\n" 
          "  remove_host           Removes a host from the list of slaves PRT is to use\n"
          "  auth_token            Add Plex auth token to configuration\n"
          "  sessions              Display current sessions\n"
          "  check_config          Checks the current configuration for errors\n")
  
  
  def main():
      # Specific usage options
      if len(sys.argv) < 2 or any((sys.argv[1] == "usage", sys.argv[1] == "help", sys.argv[1] == "-h",
              sys.argv[1] == "?",)):
          usage()
          sys.exit(-1)
  
      #user = getpass.getuser()
      #if user != 'plex':
      #    print ("Warning: You are not running as the Plex user")
      #    return
  
      # TODO: show_hosts_status to show current status across all nodes
  
      if sys.argv[1] == "get_load":
          print " ".join([str(i) for i in get_system_load_local()])
  
      elif sys.argv[1] == "get_cluster_load":
          print "Cluster Load"
          config = get_config()
          servers = config["servers"]
          for address, server in servers.items():
              load = ["%0.2f%%" % l for l in get_system_load_remote(address, server["port"], server["user"])]
              print "  %15s: %s" % (address, ", ".join(load))
  
      elif sys.argv[1] == "install":
          print "Installing Plex Remote Transcoder"
          config = get_config()
          config["ipaddress"] = raw_input("IP address of this machine: ")
          save_config(config)
  
          install_transcoder()
  
      elif sys.argv[1] == "auth_token":
          print "Adding Plex auth token"
          config = get_config()
          config["auth_token"] = get_auth_token()
          save_config(config)
  
      elif sys.argv[1] == "add_host":
          host = None
          port = None
          user = None
  
          if len(sys.argv) >= 3:
              host = sys.argv[2]
          if len(sys.argv) >= 4:
              port = sys.argv[3]
          if len(sys.argv) >= 5:
              user = sys.argv[4]
  
          if host is None:
              host = raw_input("Host: ")
          if port is None:
              port = raw_input("Port: ")
          if user is None:
              user = raw_input("User: ")
  
          print "We're going to add the following transcode host:"
          print "  Host: %s" % host
          print "  Port: %s" % port
          print "  User: %s" % user
  
          if raw_input("Proceed: [y/n]").lower() == "y":
              config = get_config()
              config["servers"][host] = {
                  "port": port,
                  "user": user
              }
  
              if save_config(config):
                  print "Host successfully added"
  
      elif sys.argv[1] == "remove_host":
          config = get_config()
          try:
              del config["servers"][sys.argv[2]]
              save_config(config)
              print "Host removed"
          except Exception, e:
              print "Error removing host: %s" % str(e)
  
      # Added version option rather than just for no options
      elif any( [sys.argv[1] == "version", sys.argv[1] == "v", sys.argv[1] == "V"] ):
          version()
          sys.exit(0)
  
      # Overwrite option (for after plex package update/upgrade)
      elif sys.argv[1] == "overwrite":
              overwrite_transcoder_after_upgrade()
              print "Transcoder overwritten successfully"
  
      elif sys.argv[1] == "sessions":
          sessions()
  
      elif sys.argv[1] == "check_config":
          check_config()
  
      # Todo: list_hosts option to show current hosts to aid add/remove_host options - Liviynz
  
      # Anything not listed shows usage
      else:
          usage()
          sys.exit(-1)
  
  