예제 #1
0
class PlexSleep:
    """ Use the plexapi to monitor:
            - client connections
            - streaming sessions
            - transcoding sessions (for things like sync)

    """
    config_filename = 'config_plex_sleep.yml'
    def __init__(self, config_filename):

        log.info(f'plex_sleep v{__version__}')
        self.load_config(config_filename)
        self.activity = time.time()
        self.baseurl = f'http://{self.server}:{self.port}'

        self.wait_for_resume()
        log.info(f"Connnecting to plex server at {self.baseurl}...")
        self.plex = PlexServer(self.baseurl, self.token)
        log.info(f"Connnected")

        self.pending_refreshes = {}
        self.watch_server()

    def load_config(self, filename):
        with open(filename) as f:
            self.config = yaml.load(f, Loader=yaml.FullLoader)

        self.user = self.config.get('user')
        self.server = self.config.get('server', 'localhost')
        self.port = self.config.get('port', '32400')
        self.timeout = self.config.get('timeout', 10*60)   # Default to 10 minutes before sleeping
        self.check_interval = self.config.get('check_interval', 60)  # Check every minute to update who's connected

        # Default scan intervals
        self.library_scan_interval = {'movie': 60*60*12,     # 12 hours,
                                      'show':  60*60*12,     # TV shows = 12 hours
                                      'artist': 60*60*48,    # Music = 2 days
                                      'photo': 60*60*24,     # Photos = 1 day,
                                      }
        scan_intervals = self.config.get('scan_interval', "movie:43200")
        for entry in scan_intervals.split(','):
            lib_type, lib_interval = entry.split(':')
            lib_type = lib_type.strip()
            lib_interval = lib_interval.strip()
            self.library_scan_interval[lib_type] = int(lib_interval)
            # Catch some common errors that a user might make
            if lib_type == 'music' or lib_type == 'mp3':
                log.warn(f'For scan_interval, you have used {lib_type}, but please use "artist" for Music libraries')
            if lib_type == 'tv' or lib_type == 'tv shows':
                log.warn(f'For scan_interval, you have used {lib_type}, but please use "show" for TV libraries')
        
        for lib_type, lib_interval in self.library_scan_interval.items():
            log.info(f'Scan interval for {lib_type} libraries: {self.library_scan_interval[lib_type]} seconds')

        # Get the token from the environment first, then look for it in the config file
        if 'PLEX_TOKEN' in os.environ:
            self.token = os.environ.get('PLEX_TOKEN')
        elif 'token' in self.config:
            self.token = self.config.get('token')
        else:
            log.error(f'No PLEX_TOKEN environment variable set or "token" statement in config file (used to connect to Plex server')
            log.error('Exiting')
            sys.exit(-1)

    def watch_server(self):
        idle_time = 0
        last_client_time = time.time()
        log.info('Started monitoring')
        while True:
            n_clients = self.get_num_clients()
            n_sess = self.get_num_sessions()
            n_trans = self.get_num_transcode_sessions()
            n_activity = self.get_activity_report()
            n = n_clients + n_sess + n_trans + n_activity
            self.refresh_libraries()

            if n>0: # People are browsing the server
                last_client_time = time.time()
                log.debug(f'Active clients:{n_clients}|sessions:{n_sess}|transcodes:{n_trans}|scans:{n_activity}')
            else:
                idle_time = time.time() - last_client_time
                if idle_time > self.timeout:
                    log.info(f'Plex server idle for {int(idle_time/60)} minutes. Suspending...')
                    if self._is_alive(self.server):
                        os.system(f"""ssh -o StrictHostKeyChecking=no {self.user}@{self.server} 'echo "sudo pm-suspend" | at now + 1 minute'""")
                    self.wait_for_suspend()
                    self.wait_for_resume()
                    log.info('resuming...')
                    last_client_time = time.time()
                else:
                    log.debug(f'Plex server has been idle for {int(idle_time/60)} minutes')
            time.sleep(self.check_interval)

    def _is_alive(self, server):
        r = ping(server, count=1, timeout=1)
        return r.success()

    def wait_for_suspend(self):
        log.info(f'waiting for {self.server} to sleep')
        while True:
            if self._is_alive(self.server):
                log.debug('ping is alive')
                time.sleep(5)
            else:
                log.info(f'{self.server} is asleep')
                return

    def wait_for_resume(self):
        log.info(f'waiting for {self.server} to awaken')
        while True:
            try:
                if self._is_alive(self.server):
                    log.info(f'{self.server} is awake')
                    return
                else:
                    raise OSError
            except OSError:
                log.debug('ping is dead, waiting...')
                time.sleep(self.check_interval)
                # Probably errno 64 Host is down
                
    def _json_query(self, end_point):
        """
            Make a custom query that returns json instead of xml like Plex's default
            end_point:  '/status/sessions'
        """
        headers = self.plex._headers()
        headers['Accept'] = 'application/json'
        url = self.plex.url(end_point)
        response = requests.get(url, headers=headers)
        return response.text

    def get_num_sessions(self):
        # Any client/app watching a stream
        return self._parse_count('/status/sessions')
        
    def get_num_clients(self):
        # Any client/app browsing the server
        return self._parse_count('/clients/')

    def get_num_transcode_sessions(self):
        # Transcodes to a player or a sync
        return self._parse_count('/transcode/sessions')

    def get_activity_report(self):
        # Any library scans running on the server are reported here
        return self._parse_count('/activities')

    def _parse_count(self, end_point):
        if self._is_alive(self.server):
            j = self._json_query(end_point)
            d = json.loads(j)
            log.debug(json.dumps(d, indent=4) )
            return int(d['MediaContainer']['size'])
        else:
            return 0

    def refresh_libraries(self):
        """Trigger a rescan of any library that was last scanned earlier than
           our library_scan_interval
        """

        # Get the library api
        end_point = '/library/sections'
        j = self._json_query(end_point)
        d = json.loads(j)
        log.debug(json.dumps(d, indent=4) )
        current_time = int(time.time())

        # Clear out any pending refresh marks that are older than 10 minutes
        #   - This is for the corner case where we marked something for refresh, it started refreshing
        #     and then it finished refreshing before this function was called again.
        #for r, start_time in dict(self.pending_refreshes).items():
            #if (current_time - start_time) > 10*60:
                #del self.pending_refreshes[r]

        for library in d['MediaContainer']['Directory']:
            last_scan = library['scannedAt']
            library_type = library['type']
            # Don't do anything if the scan interval is zero
            if self.library_scan_interval[library_type] == 0: continue

            if not library['refreshing']:
                # If Plex is not currently refreshing
                if current_time - last_scan > self.library_scan_interval.get(library_type, 60*60*24):
                    # Library last refreshed earlier than library scan interval, so mark for refresh
                    if library['key'] not in self.pending_refreshes:
                        log.info(f'Starting refresh of {library["title"]}')
                        end_point = f'/library/sections/{library["key"]}/refresh'
                        j = self._json_query(end_point)
                        self.pending_refreshes[library['key']] = current_time
                    else:
                        # We've already queued this up for a refresh so don't do anything
                        pass
                else:
                    if library['key'] in self.pending_refreshes:
                        log.info(f'Completed refresh of {library["title"]}')
                        del self.pending_refreshes[library['key']]