def upsert(self, table_name, value_dict, key_dict): trans_type = 'update' changes_before = self.connection.total_changes gen_params = lambda my_dict: [x + " = ?" for x in my_dict.keys()] update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \ " WHERE " + " AND ".join(gen_params(key_dict)) self.action(update_query, value_dict.values() + key_dict.values()) if self.connection.total_changes == changes_before: trans_type = 'insert' insert_query = ( "INSERT INTO " + table_name + " (" + ", ".join(value_dict.keys() + key_dict.keys()) + ")" + " VALUES (" + ", ".join(["?"] * len(value_dict.keys() + key_dict.keys())) + ")" ) try: self.action(insert_query, value_dict.values() + key_dict.values()) except sqlite3.IntegrityError: logger.info('Queries failed: %s and %s', update_query, insert_query) # We want to know if it was an update or insert return trans_type
def upsert(self, table_name, value_dict, key_dict): trans_type = 'update' changes_before = self.connection.total_changes gen_params = lambda my_dict: [x + " = ?" for x in my_dict.keys()] update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \ " WHERE " + " AND ".join(gen_params(key_dict)) self.action(update_query, value_dict.values() + key_dict.values()) if self.connection.total_changes == changes_before: trans_type = 'insert' insert_query = ( "INSERT INTO " + table_name + " (" + ", ".join(value_dict.keys() + key_dict.keys()) + ")" + " VALUES (" + ", ".join(["?"] * len(value_dict.keys() + key_dict.keys())) + ")") try: self.action(insert_query, value_dict.values() + key_dict.values()) except sqlite3.IntegrityError: logger.info('Queries failed: %s and %s', update_query, insert_query) # We want to know if it was an update or insert return trans_type
def shutdown(restart=False, update=False): cherrypy.engine.exit() SCHED.shutdown(wait=False) CONFIG.write() if not restart and not update: logger.info('Plex:CS is shutting down...') if update: logger.info('Plex:CS is updating...') try: versioncheck.update() except Exception as e: logger.warn('Plex:CS failed to update: %s. Restarting.', e) if CREATEPID: logger.info('Removing pidfile %s', PIDFILE) os.remove(PIDFILE) if restart: logger.info('Plex:CS is restarting...') popen_list = [sys.executable, FULL_PATH] popen_list += ARGS if '--nolaunch' not in popen_list: popen_list += ['--nolaunch'] logger.info('Restarting Plex:CS with %s', popen_list) subprocess.Popen(popen_list, cwd=os.getcwd()) os._exit(0)
def refresh_users(): logger.info("Requesting users list refresh...") result = PlexTV().get_full_users_list() monitor_db = database.MonitorDatabase() if len(result) > 0: for item in result: control_value_dict = {"user_id": item['user_id']} new_value_dict = {"username": item['username'], "thumb": item['thumb'], "email": item['email'], "is_home_user": item['is_home_user'], "is_allow_sync": item['is_allow_sync'], "is_restricted": item['is_restricted'] } # Check if we've set a custom avatar if so don't overwrite it. if item['user_id']: avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url ' 'FROM users WHERE user_id = ?', [item['user_id']]) if avatar_urls: if not avatar_urls[0]['custom_avatar_url'] or \ avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']: new_value_dict['custom_avatar_url'] = item['thumb'] else: new_value_dict['custom_avatar_url'] = item['thumb'] monitor_db.upsert('users', new_value_dict, control_value_dict) logger.info("Users list refreshed.") else: logger.warn("Unable to refresh users list.")
def daemonize(): if threading.activeCount() != 1: logger.warn( 'There are %r active threads. Daemonizing may cause' ' strange behavior.', threading.enumerate()) sys.stdout.flush() sys.stderr.flush() # Do first fork try: pid = os.fork() # @UndefinedVariable - only available in UNIX if pid != 0: sys.exit(0) except OSError as e: raise RuntimeError("1st fork failed: %s [%d]", e.strerror, e.errno) os.setsid() # Make sure I can read my own files and shut out others prev = os.umask(0) # @UndefinedVariable - only available in UNIX os.umask(prev and int('077', 8)) # Make the child a session-leader by detaching from the terminal try: pid = os.fork() # @UndefinedVariable - only available in UNIX if pid != 0: sys.exit(0) except OSError as e: raise RuntimeError("2nd fork failed: %s [%d]", e.strerror, e.errno) dev_null = file('/dev/null', 'r') os.dup2(dev_null.fileno(), sys.stdin.fileno()) si = open('/dev/null', "r") so = open('/dev/null', "a+") se = open('/dev/null', "a+") os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) pid = os.getpid() logger.info('Daemonized to PID: %d', pid) if CREATEPID: logger.info("Writing PID %d to %s", pid, PIDFILE) with file(PIDFILE, 'w') as fp: fp.write("%s\n" % pid)
def fetchData(self): logger.info('Recieved API command: %s' % self.cmd) if self.cmd and self.authenticated: methodtocall = getattr(self, "_" + self.cmd) # Let the traceback hit cherrypy so we can # see the traceback there if self.debug: methodtocall(**self.kwargs) else: try: methodtocall(**self.kwargs) except Exception as e: logger.error(traceback.format_exc()) # Im just lazy, fix me plx if self.data or isinstance(self.data, (dict, list)): if len(self.data): self.result_type = 'success' return self._out_as(self._responds(result_type=self.result_type, msg=self.msg, data=self.data))
def _out_as(self, out): if self.out_type == 'json': cherrypy.response.headers[ 'Content-Type'] = 'application/json;charset=UTF-8' try: out = json.dumps(out, indent=4, sort_keys=True) if self.callback is not None: cherrypy.response.headers[ 'Content-Type'] = 'application/javascript' # wrap with JSONP call if requested out = self.callback + '(' + out + ');' # if we fail to generate the output fake an error except Exception as e: logger.info(u"API :: " + traceback.format_exc()) out['message'] = traceback.format_exc() out['result'] = 'error' if self.out_type == 'xml': cherrypy.response.headers['Content-Type'] = 'application/xml' try: out = xmltodict.unparse(out, pretty=True) except ValueError as e: logger.error('Failed to parse xml result') try: out['message'] = e out['result'] = 'error' out = xmltodict.unparse(out, pretty=True) except Exception as e: logger.error('Failed to parse xml result error message') out = '''<?xml version="1.0" encoding="utf-8"?> <response> <message>%s</message> <data></data> <result>error</result> </response> ''' % e return out
def run(): from websocket import create_connection uri = 'ws://%s:%s/:/websockets/notifications' % ( plexcs.CONFIG.PMS_IP, plexcs.CONFIG.PMS_PORT ) # Set authentication token (if one is available) if plexcs.CONFIG.PMS_TOKEN: uri += '?X-Plex-Token=' + plexcs.CONFIG.PMS_TOKEN ws_connected = False reconnects = 0 # Try an open the websocket connection - if it fails after 15 retries fallback to polling while not ws_connected and reconnects <= 15: try: logger.info(u'Plex:CS WebSocket :: Opening websocket, connection attempt %s.' % str(reconnects + 1)) ws = create_connection(uri) reconnects = 0 ws_connected = True logger.info(u'Plex:CS WebSocket :: Ready') except IOError as e: logger.error(u'Plex:CS WebSocket :: %s.' % e) reconnects += 1 time.sleep(5) while ws_connected: try: process(*receive(ws)) # successfully received data, reset reconnects counter reconnects = 0 except websocket.WebSocketConnectionClosedException: if reconnects <= 15: reconnects += 1 # Sleep 5 between connection attempts if reconnects > 1: time.sleep(5) logger.warn(u'Plex:CS WebSocket :: Connection has closed, reconnecting...') try: ws = create_connection(uri) except IOError as e: logger.info(u'Plex:CS WebSocket :: %s.' % e) else: ws_connected = False break if not ws_connected: logger.error(u'Plex:CS WebSocket :: Connection unavailable, falling back to polling.') plexcs.POLLING_FAILOVER = True plexcs.initialize_scheduler() logger.debug(u'Plex:CS WebSocket :: Leaving thread.')
def _out_as(self, out): if self.out_type == 'json': cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8' try: out = json.dumps(out, indent=4, sort_keys=True) if self.callback is not None: cherrypy.response.headers['Content-Type'] = 'application/javascript' # wrap with JSONP call if requested out = self.callback + '(' + out + ');' # if we fail to generate the output fake an error except Exception as e: logger.info(u"API :: " + traceback.format_exc()) out['message'] = traceback.format_exc() out['result'] = 'error' if self.out_type == 'xml': cherrypy.response.headers['Content-Type'] = 'application/xml' try: out = xmltodict.unparse(out, pretty=True) except ValueError as e: logger.error('Failed to parse xml result') try: out['message'] = e out['result'] = 'error' out = xmltodict.unparse(out, pretty=True) except Exception as e: logger.error('Failed to parse xml result error message') out = '''<?xml version="1.0" encoding="utf-8"?> <response> <message>%s</message> <data></data> <result>error</result> </response> ''' % e return out
def fetchData(self): logger.info('Recieved API command: %s' % self.cmd) if self.cmd and self.authenticated: methodtocall = getattr(self, "_" + self.cmd) # Let the traceback hit cherrypy so we can # see the traceback there if self.debug: methodtocall(**self.kwargs) else: try: methodtocall(**self.kwargs) except Exception as e: logger.error(traceback.format_exc()) # Im just lazy, fix me plx if self.data or isinstance(self.data, (dict, list)): if len(self.data): self.result_type = 'success' return self._out_as( self._responds(result_type=self.result_type, msg=self.msg, data=self.data))
def schedule_job(function, name, hours=0, minutes=0, seconds=0): """ Start scheduled job if starting or restarting plexcs. Reschedule job if Interval Settings have changed. Remove job if if Interval Settings changed to 0 """ job = SCHED.get_job(name) if job: if hours == 0 and minutes == 0 and seconds == 0: SCHED.remove_job(name) logger.info("Removed background task: %s", name) elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes): SCHED.reschedule_job(name, trigger=IntervalTrigger( hours=hours, minutes=minutes, seconds=seconds)) logger.info("Re-scheduled background task: %s", name) elif hours > 0 or minutes > 0 or seconds > 0: SCHED.add_job(function, id=name, trigger=IntervalTrigger( hours=hours, minutes=minutes, seconds=seconds)) logger.info("Scheduled background task: %s", name)
def get_real_pms_url(): logger.info("Requesting URLs for server...") # Reset any current PMS_URL value plexcs.CONFIG.__setattr__('PMS_URL', '') plexcs.CONFIG.write() fallback_url = 'http://' + plexcs.CONFIG.PMS_IP + ':' + str(plexcs.CONFIG.PMS_PORT) if plexcs.CONFIG.PMS_SSL: result = PlexTV().get_server_urls(include_https=True) process_urls = True elif plexcs.CONFIG.PMS_IS_REMOTE: result = PlexTV().get_server_urls(include_https=False) process_urls = True else: process_urls = False if process_urls: if len(result) > 0: for item in result: if plexcs.CONFIG.PMS_IS_REMOTE and item['local'] == '0': plexcs.CONFIG.__setattr__('PMS_URL', item['uri']) plexcs.CONFIG.write() logger.info("Server URL retrieved.") if not plexcs.CONFIG.PMS_IS_REMOTE and item['local'] == '1': plexcs.CONFIG.__setattr__('PMS_URL', item['uri']) plexcs.CONFIG.write() logger.info("Server URL retrieved.") else: plexcs.CONFIG.__setattr__('PMS_URL', fallback_url) plexcs.CONFIG.write() logger.warn("Unable to retrieve server URLs. Using user-defined value.") else: plexcs.CONFIG.__setattr__('PMS_URL', fallback_url) plexcs.CONFIG.write()
def checkGithub(): plexcs.COMMITS_BEHIND = 0 # Get the latest version available from github logger.info('Retrieving latest version information from GitHub') url = 'https://api.github.com/repos/%s/plex-cs/commits/%s' % ( plexcs.CONFIG.GIT_USER, plexcs.CONFIG.GIT_BRANCH) version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict) if version is None: logger.warn( 'Could not get the latest version from GitHub. Are you running a local development version?' ) return plexcs.CURRENT_VERSION plexcs.LATEST_VERSION = version['sha'] logger.debug("Latest version is %s", plexcs.LATEST_VERSION) # See how many commits behind we are if not plexcs.CURRENT_VERSION: logger.info( 'You are running an unknown version of Plex:CS. Run the updater to identify your version' ) return plexcs.LATEST_VERSION if plexcs.LATEST_VERSION == plexcs.CURRENT_VERSION: logger.info('Plex:CS is up to date') return plexcs.LATEST_VERSION logger.info( 'Comparing currently installed version with latest GitHub version') url = 'https://api.github.com/repos/%s/plex-cs/compare/%s...%s' % ( plexcs.CONFIG.GIT_USER, plexcs.LATEST_VERSION, plexcs.CURRENT_VERSION) commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict) if commits is None: logger.warn('Could not get commits behind from GitHub.') return plexcs.LATEST_VERSION try: plexcs.COMMITS_BEHIND = int(commits['behind_by']) logger.debug("In total, %d commits behind", plexcs.COMMITS_BEHIND) except KeyError: logger.info( 'Cannot compare versions. Are you running a local development version?' ) plexcs.COMMITS_BEHIND = 0 if plexcs.COMMITS_BEHIND > 0: logger.info('New version is available. You are %s commits behind' % plexcs.COMMITS_BEHIND) elif plexcs.COMMITS_BEHIND == 0: logger.info('Plex:CS is up to date') return plexcs.LATEST_VERSION
def initialize(config_file): with INIT_LOCK: global CONFIG global _INITIALIZED global CURRENT_VERSION global LATEST_VERSION global UMASK global POLLING_FAILOVER CONFIG = plexcs.config.Config(config_file) assert CONFIG is not None if _INITIALIZED: return False if CONFIG.HTTP_PORT < 21 or CONFIG.HTTP_PORT > 65535: plexcs.logger.warn( 'HTTP_PORT out of bounds: 21 < %s < 65535', CONFIG.HTTP_PORT) CONFIG.HTTP_PORT = 8182 if CONFIG.HTTPS_CERT == '': CONFIG.HTTPS_CERT = os.path.join(DATA_DIR, 'server.crt') if CONFIG.HTTPS_KEY == '': CONFIG.HTTPS_KEY = os.path.join(DATA_DIR, 'server.key') if not CONFIG.LOG_DIR: CONFIG.LOG_DIR = os.path.join(DATA_DIR, 'logs') if not os.path.exists(CONFIG.LOG_DIR): try: os.makedirs(CONFIG.LOG_DIR) except OSError: CONFIG.LOG_DIR = None if not QUIET: sys.stderr.write("Unable to create the log directory. " \ "Logging to screen only.\n") # Start the logger, disable console if needed logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR, verbose=VERBOSE) if not CONFIG.CACHE_DIR: # Put the cache dir in the data dir for now CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache') if not os.path.exists(CONFIG.CACHE_DIR): try: os.makedirs(CONFIG.CACHE_DIR) except OSError as e: logger.error("Could not create cache dir '%s': %s", DATA_DIR, e) # Initialize the database logger.info('Checking to see if the database has all tables....') try: dbcheck() except Exception as e: logger.error("Can't connect to the database: %s", e) # Check if Plex:CS has a uuid if CONFIG.PMS_UUID == '' or not CONFIG.PMS_UUID: my_uuid = generate_uuid() CONFIG.__setattr__('PMS_UUID', my_uuid) CONFIG.write() # Get the currently installed version. Returns None, 'win32' or the git # hash. CURRENT_VERSION, CONFIG.GIT_BRANCH = versioncheck.getVersion() # Write current version to a file, so we know which version did work. # This allowes one to restore to that version. The idea is that if we # arrive here, most parts of Plex:CS seem to work. if CURRENT_VERSION: version_lock_file = os.path.join(DATA_DIR, "version.lock") try: with open(version_lock_file, "w") as fp: fp.write(CURRENT_VERSION) except IOError as e: logger.error("Unable to write current version to file '%s': %s", version_lock_file, e) # Check for new versions if CONFIG.CHECK_GITHUB_ON_STARTUP and CONFIG.CHECK_GITHUB: try: LATEST_VERSION = versioncheck.checkGithub() except: logger.exception("Unhandled exception") LATEST_VERSION = CURRENT_VERSION else: LATEST_VERSION = CURRENT_VERSION # Get the real PMS urls for SSL and remote access if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT: plextv.get_real_pms_url() pmsconnect.get_server_friendly_name() # Refresh the users list on startup if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP: plextv.refresh_users() # Store the original umask UMASK = os.umask(0) os.umask(UMASK) _INITIALIZED = True return True
def update(): if plexcs.INSTALL_TYPE == 'win': logger.info('Windows .exe updating not supported yet.') elif plexcs.INSTALL_TYPE == 'git': output, err = runGit('pull origin ' + plexcs.CONFIG.GIT_BRANCH) if not output: logger.error('Couldn\'t download latest version') for line in output.split('\n'): if 'Already up-to-date.' in line: logger.info('No update available, not updating') logger.info('Output: ' + str(output)) elif line.endswith('Aborting.'): logger.error('Unable to update from git: ' + line) logger.info('Output: ' + str(output)) else: tar_download_url = 'https://github.com/%s/plex-cs/tarball/%s' % ( plexcs.CONFIG.GIT_USER, plexcs.CONFIG.GIT_BRANCH) update_dir = os.path.join(plexcs.PROG_DIR, 'update') version_path = os.path.join(plexcs.PROG_DIR, 'version.txt') logger.info('Downloading update from: ' + tar_download_url) data = request.request_content(tar_download_url) if not data: logger.error( "Unable to retrieve new version from '%s', can't update", tar_download_url) return download_name = plexcs.CONFIG.GIT_BRANCH + '-github' tar_download_path = os.path.join(plexcs.PROG_DIR, download_name) # Save tar to disk with open(tar_download_path, 'wb') as f: f.write(data) # Extract the tar to update folder logger.info('Extracting file: ' + tar_download_path) tar = tarfile.open(tar_download_path) tar.extractall(update_dir) tar.close() # Delete the tar.gz logger.info('Deleting file: ' + tar_download_path) os.remove(tar_download_path) # Find update dir name update_dir_contents = [ x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x)) ] if len(update_dir_contents) != 1: logger.error("Invalid update data, update failed: " + str(update_dir_contents)) return content_dir = os.path.join(update_dir, update_dir_contents[0]) # walk temp folder and move files to main folder for dirname, dirnames, filenames in os.walk(content_dir): dirname = dirname[len(content_dir) + 1:] for curfile in filenames: old_path = os.path.join(content_dir, dirname, curfile) new_path = os.path.join(plexcs.PROG_DIR, dirname, curfile) if os.path.isfile(new_path): os.remove(new_path) os.renames(old_path, new_path) # Update version.txt try: with open(version_path, 'w') as f: f.write(str(plexcs.LATEST_VERSION)) except IOError as e: logger.error( "Unable to write current version to version.txt, update not complete: %s", e) return
def sig_handler(signum=None, frame=None): if signum is not None: logger.info("Signal %i caught, saving and exiting...", signum) shutdown()
def initialize(options): # HTTPS stuff stolen from sickbeard enable_https = options['enable_https'] https_cert = options['https_cert'] https_key = options['https_key'] if enable_https: # If either the HTTPS certificate or key do not exist, try to make # self-signed ones. if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)): if not create_https_certificates(https_cert, https_key): logger.warn("Unable to create certificate and key. Disabling " \ "HTTPS") enable_https = False if not (os.path.exists(https_cert) and os.path.exists(https_key)): logger.warn("Disabled HTTPS because of missing certificate and " \ "key.") enable_https = False options_dict = { 'server.socket_port': options['http_port'], 'server.socket_host': options['http_host'], 'server.thread_pool': 10, 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', 'tools.decode.on': True, 'log.screen': False, 'engine.autoreload.on': False, } if enable_https: options_dict['server.ssl_certificate'] = https_cert options_dict['server.ssl_private_key'] = https_key protocol = "https" else: protocol = "http" logger.info("Starting Plex:CS web server on %s://%s:%d/", protocol, options['http_host'], options['http_port']) cherrypy.config.update(options_dict) conf = { '/': { 'tools.staticdir.root': os.path.join(plexcs.PROG_DIR, 'data'), 'tools.proxy.on': options['http_proxy'] # pay attention to X-Forwarded-Proto header }, '/interfaces': { 'tools.staticdir.on': True, 'tools.staticdir.dir': "interfaces" }, '/images': { 'tools.staticdir.on': True, 'tools.staticdir.dir': "images" }, '/css': { 'tools.staticdir.on': True, 'tools.staticdir.dir': "css" }, '/js': { 'tools.staticdir.on': True, 'tools.staticdir.dir': "js" }, '/favicon.ico': { 'tools.staticfile.on': True, 'tools.staticfile.filename': os.path.join(os.path.abspath( os.curdir), "images" + os.sep + "favicon.ico") }, '/cache': { 'tools.staticdir.on': True, 'tools.staticdir.dir': plexcs.CONFIG.CACHE_DIR } } if options['http_password']: logger.info("Web server authentication is enabled, username is '%s'", options['http_username']) conf['/'].update({ 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'Plex:CS web server', 'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({ options['http_username']: options['http_password'] }) }) conf['/api'] = {'tools.auth_basic.on': False} # Prevent time-outs cherrypy.engine.timeout_monitor.unsubscribe() cherrypy.tree.mount(WebInterface(), str(options['http_root']), config=conf) try: cherrypy.process.servers.check_port(str(options['http_host']), options['http_port']) cherrypy.server.start() except IOError: sys.stderr.write('Failed to start on port: %i. Is something else running?\n' % (options['http_port'])) sys.exit(1) cherrypy.server.wait()
def initialize_scheduler(): """ Start the scheduled background tasks. Re-schedule if interval settings changed. """ with SCHED_LOCK: # Check if scheduler should be started start_jobs = not len(SCHED.get_jobs()) # Update check if CONFIG.CHECK_GITHUB_INTERVAL and CONFIG.CHECK_GITHUB: minutes = CONFIG.CHECK_GITHUB_INTERVAL else: minutes = 0 schedule_job(versioncheck.checkGithub, 'Check GitHub for updates', hours=0, minutes=minutes) # Start checking for new sessions at set interval if CONFIG.MONITORING_INTERVAL: # Our interval should never be less than 30 seconds if CONFIG.MONITORING_INTERVAL > 30: seconds = CONFIG.MONITORING_INTERVAL else: seconds = 30 else: seconds = 0 if CONFIG.PMS_IP and CONFIG.PMS_TOKEN: schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs', hours=12, minutes=0, seconds=0) schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex Server Name', hours=12, minutes=0, seconds=0) if CONFIG.NOTIFY_RECENTLY_ADDED: schedule_job(activity_pinger.check_recently_added, 'Check for recently added items', hours=0, minutes=0, seconds=seconds) else: schedule_job(activity_pinger.check_recently_added, 'Check for recently added items', hours=0, minutes=0, seconds=0) if CONFIG.MONITOR_REMOTE_ACCESS: schedule_job(activity_pinger.check_server_response, 'Check for server response', hours=0, minutes=0, seconds=seconds) else: schedule_job(activity_pinger.check_server_response, 'Check for server response', hours=0, minutes=0, seconds=0) # If we're not using websockets then fall back to polling if not CONFIG.MONITORING_USE_WEBSOCKET or POLLING_FAILOVER: schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=seconds) # Refresh the users list if CONFIG.REFRESH_USERS_INTERVAL: hours = CONFIG.REFRESH_USERS_INTERVAL else: hours = 0 if CONFIG.PMS_TOKEN: schedule_job(plextv.refresh_users, 'Refresh users list', hours=hours, minutes=0, seconds=0) # Start scheduler if start_jobs and len(SCHED.get_jobs()): try: SCHED.start() except Exception as e: logger.info(e)
def check_active_sessions(ws_request=False): with monitor_lock: pms_connect = pmsconnect.PmsConnect() session_list = pms_connect.get_current_activity() monitor_db = database.MonitorDatabase() monitor_process = activity_processor.ActivityProcessor() # logger.debug(u"Plex:CS Monitor :: Checking for active streams.") global int_ping_count if session_list: int_ping_count = 0 media_container = session_list['sessions'] # Check our temp table for what we must do with the new streams db_streams = monitor_db.select('SELECT started, session_key, rating_key, media_type, title, parent_title, ' 'grandparent_title, user_id, user, friendly_name, ip_address, player, ' 'platform, machine_id, parent_rating_key, grandparent_rating_key, state, ' 'view_offset, duration, video_decision, audio_decision, width, height, ' 'container, video_codec, audio_codec, bitrate, video_resolution, ' 'video_framerate, aspect_ratio, audio_channels, transcode_protocol, ' 'transcode_container, transcode_video_codec, transcode_audio_codec, ' 'transcode_audio_channels, transcode_width, transcode_height, ' 'paused_counter, last_paused ' 'FROM sessions') for stream in db_streams: if any(d['session_key'] == str(stream['session_key']) and d['rating_key'] == str(stream['rating_key']) for d in media_container): # The user's session is still active for session in media_container: if session['session_key'] == str(stream['session_key']) and \ session['rating_key'] == str(stream['rating_key']): # The user is still playing the same media item # Here we can check the play states if session['state'] != stream['state']: if session['state'] == 'paused': # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='pause')).start() if session['state'] == 'playing' and stream['state'] == 'paused': # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='resume')).start() if stream['state'] == 'paused' and not ws_request: # The stream is still paused so we need to increment the paused_counter # Using the set config parameter as the interval, probably not the most accurate but # it will have to do for now. If it's a websocket request don't use this method. paused_counter = int(stream['paused_counter']) + plexcs.CONFIG.MONITORING_INTERVAL monitor_db.action('UPDATE sessions SET paused_counter = ? ' 'WHERE session_key = ? AND rating_key = ?', [paused_counter, stream['session_key'], stream['rating_key']]) if session['state'] == 'buffering' and plexcs.CONFIG.BUFFER_THRESHOLD > 0: # The stream is buffering so we need to increment the buffer_count # We're going just increment on every monitor ping, # would be difficult to keep track otherwise monitor_db.action('UPDATE sessions SET buffer_count = buffer_count + 1 ' 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) # Check the current buffer count and last buffer to determine if we should notify buffer_values = monitor_db.select('SELECT buffer_count, buffer_last_triggered ' 'FROM sessions ' 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) if buffer_values[0]['buffer_count'] >= plexcs.CONFIG.BUFFER_THRESHOLD: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions # Our first buffer notification if buffer_values[0]['buffer_count'] == plexcs.CONFIG.BUFFER_THRESHOLD: logger.info(u"Plex:CS Monitor :: User '%s' has triggered a buffer warning." % stream['user']) # Set the buffer trigger time monitor_db.action('UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='buffer')).start() else: # Subsequent buffer notifications after wait time if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \ plexcs.CONFIG.BUFFER_WAIT: logger.info(u"Plex:CS Monitor :: User '%s' has triggered multiple buffer warnings." % stream['user']) # Set the buffer trigger time monitor_db.action('UPDATE sessions ' 'SET buffer_last_triggered = strftime("%s","now") ' 'WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='buffer')).start() logger.debug(u"Plex:CS Monitor :: Stream buffering. Count is now %s. Last triggered %s." % (buffer_values[0]['buffer_count'], buffer_values[0]['buffer_last_triggered'])) # Check if the user has reached the offset in the media we defined as the "watched" percent # Don't trigger if state is buffer as some clients push the progress to the end when # buffering on start. if session['view_offset'] and session['duration'] and session['state'] != 'buffering': if helpers.get_percent(session['view_offset'], session['duration']) > plexcs.CONFIG.NOTIFY_WATCHED_PERCENT: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='watched')).start() else: # The user has stopped playing a stream logger.debug(u"Plex:CS Monitor :: Removing sessionKey %s ratingKey %s from session queue" % (stream['session_key'], stream['rating_key'])) monitor_db.action('DELETE FROM sessions WHERE session_key = ? AND rating_key = ?', [stream['session_key'], stream['rating_key']]) # Check if the user has reached the offset in the media we defined as the "watched" percent if stream['view_offset'] and stream['duration']: if helpers.get_percent(stream['view_offset'], stream['duration']) > plexcs.CONFIG.NOTIFY_WATCHED_PERCENT: # Push any notifications - # Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='watched')).start() # Push any notifications - Push it on it's own thread so we don't hold up our db actions threading.Thread(target=notification_handler.notify, kwargs=dict(stream_data=stream, notify_action='stop')).start() # Write the item history on playback stop monitor_process.write_session_history(session=stream) # Process the newly received session data for session in media_container: monitor_process.write_session(session) else: logger.debug(u"Plex:CS Monitor :: Unable to read session list.") int_ping_count += 1 logger.warn(u"Plex:CS Monitor :: Unable to get an internal response from the server, ping attempt %s." \ % str(int_ping_count)) if int_ping_count == 3: # Fire off notifications threading.Thread(target=notification_handler.notify_timeline, kwargs=dict(notify_action='intdown')).start()
def main(): """ Plex:CS application entry point. Parses arguments, setups encoding and initializes the application. """ # Fixed paths to Plex:CS if hasattr(sys, 'frozen'): plexcs.FULL_PATH = os.path.abspath(sys.executable) else: plexcs.FULL_PATH = os.path.abspath(__file__) plexcs.PROG_DIR = os.path.dirname(plexcs.FULL_PATH) plexcs.ARGS = sys.argv[1:] # From sickbeard plexcs.SYS_PLATFORM = sys.platform plexcs.SYS_ENCODING = None try: locale.setlocale(locale.LC_ALL, "") plexcs.SYS_ENCODING = locale.getpreferredencoding() except (locale.Error, IOError): pass # for OSes that are poorly configured I'll just force UTF-8 if not plexcs.SYS_ENCODING or plexcs.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): plexcs.SYS_ENCODING = 'UTF-8' # Set up and gather command line arguments parser = argparse.ArgumentParser( description= 'A Python based monitoring and tracking tool for Plex Media Server.') parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity') parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging') parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon') parser.add_argument('-p', '--port', type=int, help='Force Plex:CS to run on a specified port') parser.add_argument( '--datadir', help='Specify a directory where to store your data files') parser.add_argument('--config', help='Specify a config file to use') parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup') parser.add_argument( '--pidfile', help='Create a pid file (only relevant when running as a daemon)') args = parser.parse_args() if args.verbose: plexcs.VERBOSE = True if args.quiet: plexcs.QUIET = True # Do an intial setup of the logger. logger.initLogger(console=not plexcs.QUIET, log_dir=False, verbose=plexcs.VERBOSE) if args.daemon: if sys.platform == 'win32': sys.stderr.write( "Daemonizing not supported under Windows, starting normally\n") else: plexcs.DAEMON = True plexcs.QUIET = True if args.pidfile: plexcs.PIDFILE = str(args.pidfile) # If the pidfile already exists, plexcs may still be running, so # exit if os.path.exists(plexcs.PIDFILE): raise SystemExit("PID file '%s' already exists. Exiting." % plexcs.PIDFILE) # The pidfile is only useful in daemon mode, make sure we can write the # file properly if plexcs.DAEMON: plexcs.CREATEPID = True try: with open(plexcs.PIDFILE, 'w') as fp: fp.write("pid\n") except IOError as e: raise SystemExit("Unable to write PID file: %s", e) else: logger.warn("Not running in daemon mode. PID file creation " \ "disabled.") # Determine which data directory and config file to use if args.datadir: plexcs.DATA_DIR = args.datadir else: plexcs.DATA_DIR = plexcs.PROG_DIR if args.config: config_file = args.config else: config_file = os.path.join(plexcs.DATA_DIR, 'config.ini') # Try to create the DATA_DIR if it doesn't exist if not os.path.exists(plexcs.DATA_DIR): try: os.makedirs(plexcs.DATA_DIR) except OSError: raise SystemExit('Could not create data directory: ' + plexcs.DATA_DIR + '. Exiting....') # Make sure the DATA_DIR is writeable if not os.access(plexcs.DATA_DIR, os.W_OK): raise SystemExit('Cannot write to the data directory: ' + plexcs.DATA_DIR + '. Exiting...') # Put the database in the DATA_DIR plexcs.DB_FILE = os.path.join(plexcs.DATA_DIR, 'plexcs.db') # Read config and start logging plexcs.initialize(config_file) if plexcs.DAEMON: plexcs.daemonize() # Force the http port if neccessary if args.port: http_port = args.port logger.info('Using forced web server port: %i', http_port) else: http_port = int(plexcs.CONFIG.HTTP_PORT) # Check if pyOpenSSL is installed. It is required for certificate generation # and for CherryPy. if plexcs.CONFIG.ENABLE_HTTPS: try: import OpenSSL except ImportError: logger.warn("The pyOpenSSL module is missing. Install this " \ "module to enable HTTPS. HTTPS will be disabled.") plexcs.CONFIG.ENABLE_HTTPS = False # Try to start the server. Will exit here is address is already in use. web_config = { 'http_port': http_port, 'http_host': plexcs.CONFIG.HTTP_HOST, 'http_root': plexcs.CONFIG.HTTP_ROOT, 'http_proxy': plexcs.CONFIG.HTTP_PROXY, 'enable_https': plexcs.CONFIG.ENABLE_HTTPS, 'https_cert': plexcs.CONFIG.HTTPS_CERT, 'https_key': plexcs.CONFIG.HTTPS_KEY, 'http_username': plexcs.CONFIG.HTTP_USERNAME, 'http_password': plexcs.CONFIG.HTTP_PASSWORD, } webstart.initialize(web_config) # Start the background threads plexcs.start() # Open connection for websocket if plexcs.CONFIG.MONITORING_USE_WEBSOCKET: try: web_socket.start_thread() except: logger.warn(u"Websocket :: Unable to open connection.") # Fallback to polling plexcs.POLLING_FAILOVER = True plexcs.initialize_scheduler() # Open webbrowser if plexcs.CONFIG.LAUNCH_BROWSER and not args.nolaunch: plexcs.launch_browser(plexcs.CONFIG.HTTP_HOST, http_port, plexcs.CONFIG.HTTP_ROOT) # Wait endlessy for a signal to happen while True: if not plexcs.SIGNAL: try: time.sleep(1) except KeyboardInterrupt: plexcs.SIGNAL = 'shutdown' else: logger.info('Received signal: %s', plexcs.SIGNAL) if plexcs.SIGNAL == 'shutdown': plexcs.shutdown() elif plexcs.SIGNAL == 'restart': plexcs.shutdown(restart=True) else: plexcs.shutdown(restart=True, update=True) plexcs.SIGNAL = None
def main(): """ Plex:CS application entry point. Parses arguments, setups encoding and initializes the application. """ # Fixed paths to Plex:CS if hasattr(sys, 'frozen'): plexcs.FULL_PATH = os.path.abspath(sys.executable) else: plexcs.FULL_PATH = os.path.abspath(__file__) plexcs.PROG_DIR = os.path.dirname(plexcs.FULL_PATH) plexcs.ARGS = sys.argv[1:] # From sickbeard plexcs.SYS_PLATFORM = sys.platform plexcs.SYS_ENCODING = None try: locale.setlocale(locale.LC_ALL, "") plexcs.SYS_ENCODING = locale.getpreferredencoding() except (locale.Error, IOError): pass # for OSes that are poorly configured I'll just force UTF-8 if not plexcs.SYS_ENCODING or plexcs.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): plexcs.SYS_ENCODING = 'UTF-8' # Set up and gather command line arguments parser = argparse.ArgumentParser( description='A Python based monitoring and tracking tool for Plex Media Server.') parser.add_argument( '-v', '--verbose', action='store_true', help='Increase console logging verbosity') parser.add_argument( '-q', '--quiet', action='store_true', help='Turn off console logging') parser.add_argument( '-d', '--daemon', action='store_true', help='Run as a daemon') parser.add_argument( '-p', '--port', type=int, help='Force Plex:CS to run on a specified port') parser.add_argument( '--datadir', help='Specify a directory where to store your data files') parser.add_argument('--config', help='Specify a config file to use') parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup') parser.add_argument( '--pidfile', help='Create a pid file (only relevant when running as a daemon)') args = parser.parse_args() if args.verbose: plexcs.VERBOSE = True if args.quiet: plexcs.QUIET = True # Do an intial setup of the logger. logger.initLogger(console=not plexcs.QUIET, log_dir=False, verbose=plexcs.VERBOSE) if args.daemon: if sys.platform == 'win32': sys.stderr.write( "Daemonizing not supported under Windows, starting normally\n") else: plexcs.DAEMON = True plexcs.QUIET = True if args.pidfile: plexcs.PIDFILE = str(args.pidfile) # If the pidfile already exists, plexcs may still be running, so # exit if os.path.exists(plexcs.PIDFILE): raise SystemExit("PID file '%s' already exists. Exiting." % plexcs.PIDFILE) # The pidfile is only useful in daemon mode, make sure we can write the # file properly if plexcs.DAEMON: plexcs.CREATEPID = True try: with open(plexcs.PIDFILE, 'w') as fp: fp.write("pid\n") except IOError as e: raise SystemExit("Unable to write PID file: %s", e) else: logger.warn("Not running in daemon mode. PID file creation " \ "disabled.") # Determine which data directory and config file to use if args.datadir: plexcs.DATA_DIR = args.datadir else: plexcs.DATA_DIR = plexcs.PROG_DIR if args.config: config_file = args.config else: config_file = os.path.join(plexcs.DATA_DIR, 'config.ini') # Try to create the DATA_DIR if it doesn't exist if not os.path.exists(plexcs.DATA_DIR): try: os.makedirs(plexcs.DATA_DIR) except OSError: raise SystemExit( 'Could not create data directory: ' + plexcs.DATA_DIR + '. Exiting....') # Make sure the DATA_DIR is writeable if not os.access(plexcs.DATA_DIR, os.W_OK): raise SystemExit( 'Cannot write to the data directory: ' + plexcs.DATA_DIR + '. Exiting...') # Put the database in the DATA_DIR plexcs.DB_FILE = os.path.join(plexcs.DATA_DIR, 'plexcs.db') # Read config and start logging plexcs.initialize(config_file) if plexcs.DAEMON: plexcs.daemonize() # Force the http port if neccessary if args.port: http_port = args.port logger.info('Using forced web server port: %i', http_port) else: http_port = int(plexcs.CONFIG.HTTP_PORT) # Check if pyOpenSSL is installed. It is required for certificate generation # and for CherryPy. if plexcs.CONFIG.ENABLE_HTTPS: try: import OpenSSL except ImportError: logger.warn("The pyOpenSSL module is missing. Install this " \ "module to enable HTTPS. HTTPS will be disabled.") plexcs.CONFIG.ENABLE_HTTPS = False # Try to start the server. Will exit here is address is already in use. web_config = { 'http_port': http_port, 'http_host': plexcs.CONFIG.HTTP_HOST, 'http_root': plexcs.CONFIG.HTTP_ROOT, 'http_proxy': plexcs.CONFIG.HTTP_PROXY, 'enable_https': plexcs.CONFIG.ENABLE_HTTPS, 'https_cert': plexcs.CONFIG.HTTPS_CERT, 'https_key': plexcs.CONFIG.HTTPS_KEY, 'http_username': plexcs.CONFIG.HTTP_USERNAME, 'http_password': plexcs.CONFIG.HTTP_PASSWORD, } webstart.initialize(web_config) # Start the background threads plexcs.start() # Open connection for websocket if plexcs.CONFIG.MONITORING_USE_WEBSOCKET: try: web_socket.start_thread() except: logger.warn(u"Websocket :: Unable to open connection.") # Fallback to polling plexcs.POLLING_FAILOVER = True plexcs.initialize_scheduler() # Open webbrowser if plexcs.CONFIG.LAUNCH_BROWSER and not args.nolaunch: plexcs.launch_browser(plexcs.CONFIG.HTTP_HOST, http_port, plexcs.CONFIG.HTTP_ROOT) # Wait endlessy for a signal to happen while True: if not plexcs.SIGNAL: try: time.sleep(1) except KeyboardInterrupt: plexcs.SIGNAL = 'shutdown' else: logger.info('Received signal: %s', plexcs.SIGNAL) if plexcs.SIGNAL == 'shutdown': plexcs.shutdown() elif plexcs.SIGNAL == 'restart': plexcs.shutdown(restart=True) else: plexcs.shutdown(restart=True, update=True) plexcs.SIGNAL = None
def checkGithub(): plexcs.COMMITS_BEHIND = 0 # Get the latest version available from github logger.info("Retrieving latest version information from GitHub") url = "https://api.github.com/repos/%s/plex-cs/commits/%s" % (plexcs.CONFIG.GIT_USER, plexcs.CONFIG.GIT_BRANCH) version = request.request_json(url, timeout=20, validator=lambda x: type(x) == dict) if version is None: logger.warn("Could not get the latest version from GitHub. Are you running a local development version?") return plexcs.CURRENT_VERSION plexcs.LATEST_VERSION = version["sha"] logger.debug("Latest version is %s", plexcs.LATEST_VERSION) # See how many commits behind we are if not plexcs.CURRENT_VERSION: logger.info("You are running an unknown version of Plex:CS. Run the updater to identify your version") return plexcs.LATEST_VERSION if plexcs.LATEST_VERSION == plexcs.CURRENT_VERSION: logger.info("Plex:CS is up to date") return plexcs.LATEST_VERSION logger.info("Comparing currently installed version with latest GitHub version") url = "https://api.github.com/repos/%s/plex-cs/compare/%s...%s" % ( plexcs.CONFIG.GIT_USER, plexcs.LATEST_VERSION, plexcs.CURRENT_VERSION, ) commits = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict) if commits is None: logger.warn("Could not get commits behind from GitHub.") return plexcs.LATEST_VERSION try: plexcs.COMMITS_BEHIND = int(commits["behind_by"]) logger.debug("In total, %d commits behind", plexcs.COMMITS_BEHIND) except KeyError: logger.info("Cannot compare versions. Are you running a local development version?") plexcs.COMMITS_BEHIND = 0 if plexcs.COMMITS_BEHIND > 0: logger.info("New version is available. You are %s commits behind" % plexcs.COMMITS_BEHIND) elif plexcs.COMMITS_BEHIND == 0: logger.info("Plex:CS is up to date") return plexcs.LATEST_VERSION
def update(): if plexcs.INSTALL_TYPE == "win": logger.info("Windows .exe updating not supported yet.") elif plexcs.INSTALL_TYPE == "git": output, err = runGit("pull origin " + plexcs.CONFIG.GIT_BRANCH) if not output: logger.error("Couldn't download latest version") for line in output.split("\n"): if "Already up-to-date." in line: logger.info("No update available, not updating") logger.info("Output: " + str(output)) elif line.endswith("Aborting."): logger.error("Unable to update from git: " + line) logger.info("Output: " + str(output)) else: tar_download_url = "https://github.com/%s/plex-cs/tarball/%s" % ( plexcs.CONFIG.GIT_USER, plexcs.CONFIG.GIT_BRANCH, ) update_dir = os.path.join(plexcs.PROG_DIR, "update") version_path = os.path.join(plexcs.PROG_DIR, "version.txt") logger.info("Downloading update from: " + tar_download_url) data = request.request_content(tar_download_url) if not data: logger.error("Unable to retrieve new version from '%s', can't update", tar_download_url) return download_name = plexcs.CONFIG.GIT_BRANCH + "-github" tar_download_path = os.path.join(plexcs.PROG_DIR, download_name) # Save tar to disk with open(tar_download_path, "wb") as f: f.write(data) # Extract the tar to update folder logger.info("Extracting file: " + tar_download_path) tar = tarfile.open(tar_download_path) tar.extractall(update_dir) tar.close() # Delete the tar.gz logger.info("Deleting file: " + tar_download_path) os.remove(tar_download_path) # Find update dir name update_dir_contents = [x for x in os.listdir(update_dir) if os.path.isdir(os.path.join(update_dir, x))] if len(update_dir_contents) != 1: logger.error("Invalid update data, update failed: " + str(update_dir_contents)) return content_dir = os.path.join(update_dir, update_dir_contents[0]) # walk temp folder and move files to main folder for dirname, dirnames, filenames in os.walk(content_dir): dirname = dirname[len(content_dir) + 1 :] for curfile in filenames: old_path = os.path.join(content_dir, dirname, curfile) new_path = os.path.join(plexcs.PROG_DIR, dirname, curfile) if os.path.isfile(new_path): os.remove(new_path) os.renames(old_path, new_path) # Update version.txt try: with open(version_path, "w") as f: f.write(str(plexcs.LATEST_VERSION)) except IOError as e: logger.error("Unable to write current version to version.txt, update not complete: %s", e) return