def play_trailers(): # type: () -> None """ :return: """ my_trailer_dialog = None black_background = None exiting_playing_movie = False try: black_background = BlackBackground.get_instance() black_background.show() legal_info = LegalInfo('legal.xml', Constants.ADDON_PATH, 'Default') legal_info.show() Monitor.throw_exception_if_abort_requested(timeout=10.0) legal_info.close() legal_info.destroy() my_trailer_dialog = TrailerDialog('script-trailerwindow.xml', Constants.ADDON_PATH, 'Default') exiting_playing_movie = my_trailer_dialog.doModal() finally: if my_trailer_dialog is not None: del my_trailer_dialog my_trailer_dialog = None if black_background is not None: black_background.destroy() del black_background black_background = None if exiting_playing_movie: if logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): logger.debug('ReplaceWindow(12005)') xbmc.executebuiltin('ReplaceWindow(12005)')
def returned_trailer(cls, data): # type: (Any) -> None """ Front-end receives trailer from back-end :param data: :return: """ try: Monitor.throw_exception_if_abort_requested() cls._next_trailer = data.get('trailer', None) cls._status = data.get('status', None) if cls._status == FrontendBridgeStatus.BUSY: Monitor.throw_exception_if_abort_requested(timeout=2.0) elif cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): if cls._next_trailer is None: title = 'No Trailer Received' else: title = cls._next_trailer.get(Movie.TITLE, 'No Title') cls._logger.debug_extra_verbose('status:', cls._status, 'received trailer for:', title) except AbortException: reraise(*sys.exc_info()) except Exception as e: cls._logger.exception('')
def run_worker(self) -> None: clz = RunCommand rc = 0 env = os.environ.copy() try: if xbmc.getCondVisibility('System.Platform.Windows'): # Prevent console for ffmpeg from opening self.process = subprocess.Popen( self.args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, universal_newlines=True, env=env, close_fds=True, creationflags=subprocess.DETACHED_PROCESS) else: self.process = subprocess.Popen( self.args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, universal_newlines=True, env=env, close_fds=True) self.stdout_thread = threading.Thread(target=self.stdout_reader, name='normalize stdout reader') Monitor.throw_exception_if_abort_requested() self.stdout_thread.start() self.stderr_thread = threading.Thread(target=self.stderr_reader, name='normalize stderr reader') self.stderr_thread.start() except AbortException as e: pass # Let thread die except Exception as e: clz.logger.exception(e)
def save_cache(cls): # type: () -> None """ :return: """ path = os.path.join(Settings.get_remote_db_cache_path(), 'index', 'tmdb_discovery_parameters.json') path = xbmcvfs.validatePath(path) parent_dir, file_name = os.path.split(path) if not os.path.exists(parent_dir): DiskUtils.create_path_if_needed(parent_dir) with CacheIndex.lock: try: Monitor.throw_exception_if_abort_requested() with io.open( path, mode='wt', newline=None, encoding='utf-8', ) as cacheFile: json_text = cls.to_json() cacheFile.write(json_text) cacheFile.flush() except AbortException: reraise(*sys.exc_info()) except (IOError) as e: cls._logger.exception('') except Exception as e: cls._logger.exception('')
def get(self, block: bool = True, timeout: Optional[float] = None) -> Union[MovieType, None]: """ :param block: :param timeout: :return: """ if not block: timeout = 0 if timeout is None: timeout = float(60 * 60 * 24 * 365) # A year time_remaining = timeout time_chunk = 0.01 finished = False item = None while not finished: try: with self._lock: item = self._wrapped_queue.get(block=False) finished = True except KodiQueue.Empty: Monitor.throw_exception_if_abort_requested(timeout=time_chunk) time_remaining -= time_chunk if time_remaining <= 0: raise KodiQueue.Empty return item
def load_trailers() -> None: """ Start up the configured trailer discovery threads. Called whenever settings have changed to start any threads that have just ben enabled. :return: """ module_logger.enter() if Settings.get_include_library_trailers(): lib_instance = DiscoverLibraryMovies() lib_instance.discover_basic_information() # Manufacture trailer entries for folders which contain trailer # files. Note that files are assumed to be videos. if Settings.get_include_trailer_folders(): DiscoverFolderTrailers().discover_basic_information() if Settings.get_include_itunes_trailers(): DiscoverItunesMovies().discover_basic_information() if Settings.get_include_tmdb_trailers(): DiscoverTmdbMovies().discover_basic_information() if Settings.is_include_tfh_trailers(): DiscoverTFHMovies().discover_basic_information() Monitor.throw_exception_if_abort_requested(timeout=1.0) Monitor.set_startup_complete()
def get_from_fetch_queue(self, player_starving: bool = False) -> MovieType: """ :return: """ clz = AbstractMovieData # if self.logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): # self.logger.debug_extra_verbose('starving:', player_starving) self.load_fetch_queue() trailer = None if self._trailers_to_fetch_queue.empty(): if self.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): self.logger.debug_verbose(': empty') while trailer is None: try: if player_starving: trailer = self.get_from_starvation_queue() if trailer is None: trailer = self._trailers_to_fetch_queue.get(timeout=0.5) except KodiQueue.Empty: Monitor.throw_exception_if_abort_requested() if self.logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): self.logger.debug_extra_verbose( 'Got:', trailer[Movie.TITLE], 'from fetch queue') return trailer
def delay_between_transactions(cls, source: str, video: bool) -> None: if video: delay_range = DOWNLOAD_VIDEO_DELAY_BY_SOURCE[source] else: delay_range = DOWNLOAD_INFO_DELAY_BY_SOURCE[source] delay = cls.get_delay(delay_range) # min_time_between_requests = datetime.timedelta(seconds=10.0) try: cls.get_lock(source) # Block new transaction # HAVE LOCK if source == Movie.ITUNES_SOURCE: waited: datetime.timedelta = ( datetime.datetime.now() - cls._last_itunes_request_timestamp) else: waited: datetime.timedelta = ( datetime.datetime.now() - cls._last_youtube_request_timestamp) time_to_wait = delay - waited.total_seconds() if time_to_wait > 0.0: Monitor.throw_exception_if_abort_requested(time_to_wait) finally: cls.release_lock(source) # LOCK RELEASED if source == Movie.ITUNES_SOURCE: cls._last_itunes_request_timestamp = datetime.datetime.now() else: cls._last_youtube_request_timestamp = datetime.datetime.now()
def status_hook(self, status: Dict[str, str]) -> None: clz = BaseInfoHook if Monitor.is_abort_requested(): self._callback._error = 99 # Kill YoutubeDL Monitor.throw_exception_if_abort_requested() status_str = status.get('status', 'missing status') if status_str is None: clz._logger.debug('Missing status indication') elif status_str == 'downloading': self._download_eta = status.get('eta', 0) # In seconds elif status_str == 'error': clz._logger.error('Status:', str(status)) self.error_lines.append('Error downloading') self._error = VideoDownloader.DOWNLOAD_ERROR elif status_str == 'finished': filename = status.get('filename') tmpfilename = status.get('tmpfilename') downloaded_bytes = status.get('downloaded_bytes') total_bytes = status.get('total_bytes') total_bytes_estimate = status.get('total_bytes_estimate') elapsed = status.get('elapsed') eta = status.get('eta') speed = status.get('speed') fragment_index = status.get('fragment_index') fragment_count = status.get('fragment_count') clz._logger.debug('Finished')
def load_unprocessed_movie_cache(cls): # type: () -> None """ :return: """ path = os.path.join(Settings.get_remote_db_cache_path(), 'index', 'tmdb_unprocessed_movies.json') path = xbmcvfs.validatePath(path) try: parent_dir, file_name = os.path.split(path) DiskUtils.create_path_if_needed(parent_dir) with CacheIndex.lock: if os.path.exists(path): with io.open(path, mode='rt', newline=None, encoding='utf-8') as cacheFile: cls._unprocessed_movies = json.load( cacheFile, encoding='utf-8', object_hook=CacheIndex.datetime_parser) cls.last_saved_movie_timestamp = None cls._unprocessed_movie_changes = 0 else: cls._unprocessed_movies = {} Monitor.throw_exception_if_abort_requested() except AbortException: reraise(*sys.exc_info()) except IOError as e: CacheIndex.logger().exception('') except JSONDecodeError as e: os.remove(path) except Exception as e: CacheIndex.logger().exception('')
def play(self, item="", listitem=None, windowed=False, startpos=-1): # type: (Union[PlayList], 'xbmcgui.ListItem', bool, int) ->None """ Play a item. :param item: [opt] string - filename, url or playlist :param listitem: [opt] listitem - used with setInfo() to set different infolabels. :param windowed: [opt] bool - true=play video windowed, false=play users preference.(default) :param startpos: [opt] int - starting position when playing a playlist. Default = -1 If item is not given then the Player will try to play the current item in the current playlist. You can use the above as keywords for arguments and skip certain optional arguments. Once you use a keyword, all following arguments require the keyword. Example:: listitem = xbmcgui.ListItem('Ironman') listitem.setInfo('video', {'Title': 'Ironman', 'Genre': 'Science Fiction'}) xbmc.Player().play(url, listitem, windowed) xbmc.Player().play(playlist, listitem, windowed, startpos) """ local_class = AdvancedPlayer Monitor.throw_exception_if_abort_requested() if type(self).DEBUG_MONITOR: local_class._logger.enter() super().play(item, listitem, windowed, startpos) self.enableAdvancedMonitoring()
def __next__(self): # type: () -> Union[dict, None] """ :return: """ movie = None try: finished = False attempt = 0 while not finished: try: attempt += 1 movie = self._do_next() finished = True except RestartDiscoveryException: Monitor.throw_exception_if_abort_requested(timeout=0.10) if self.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): self.logger.debug_verbose( 'Rediscovery in progress. attempt:', attempt) except Exception as e: self.logger.exception('') reraise(*sys.exc_info()) return movie
def save_unprocessed_movie_cache(cls, flush=False): # type: (bool) -> None """ :param flush: :return: """ # TODO: Should use lock here, review locking with cls.lock: if cls._unprocessed_movie_changes == 0: return if (not flush and # Constants.TRAILER_CACHE_FLUSH_UPDATES) (cls._unprocessed_movie_changes < 10) and (datetime.datetime.now() - \ cls._last_saved_unprocessed_movie_timestamp) < datetime.timedelta(minutes=5)): return path = os.path.join(Settings.get_remote_db_cache_path(), 'index', 'tmdb_unprocessed_movies.json') path = xbmcvfs.validatePath(path) parent_dir, file_name = os.path.split(path) if not os.path.exists(parent_dir): DiskUtils.create_path_if_needed(parent_dir) try: with io.open( path, mode='wt', newline=None, encoding='utf-8', ) as cacheFile: # TODO: Need ability to interrupt when ABORT. Object_handler # not a valid arg to dumps json_text = json.dumps(cls.get_unprocessed_movies(), encoding='utf-8', ensure_ascii=False, default=CacheIndex.handler, indent=3, sort_keys=True) cacheFile.write(json_text) cacheFile.flush() cls._last_saved_unprocessed_movie_timestamp = datetime.datetime.now( ) cls._unprocessed_movie_changes = 0 Monitor.throw_exception_if_abort_requested() except AbortException: reraise(*sys.exc_info()) except IOError as e: cls.logger().exception('') except Exception as e: cls.logger().exception('')
def abort_checker(dct: Dict[str, Any]) -> Dict[str, Any]: """ :param dct: :return: """ Monitor.throw_exception_if_abort_requested() return dct
def add_to_ready_to_play_queue(self, movie: MovieType) -> None: """ :param movie: :return: """ clz = PlayableTrailersContainer if clz.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): if Movie.TITLE not in movie: clz.logger.warning('Invalid movie entry. Missing title: ', str(movie)) Debug.validate_detailed_movie_properties(movie, stack_trace=False) try: title = movie[Movie.TITLE] if title not in clz._recently_played_trailers: clz._recently_played_trailers[title] = movie if len(clz._recently_played_trailers) > 10: clz._recently_played_trailers.popitem() else: if clz.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): clz.logger.debug_verbose( f'Movie: {title} played recently, skipping') return except Exception as e: clz.logger.exception(e) if clz.logger.isEnabledFor(LazyLogger.DEBUG): clz.logger.debug_verbose('movie:', movie[Movie.TITLE], 'queue empty:', self._ready_to_play_queue.empty(), 'full:', self._ready_to_play_queue.full()) finished = False waited = 0 while not finished: try: self._ready_to_play_queue.put(movie, block=True, timeout=0.05) finished = True self._number_of_added_trailers += 1 except (queue.Full): waited += 1 Monitor.throw_exception_if_abort_requested(timeout=0.5) if not clz._any_trailers_available_to_play.isSet(): if clz.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): clz.logger.debug_verbose( 'Setting _any_trailers_available_to_play') clz._any_trailers_available_to_play.set() self._is_playable_trailers.set() if clz.logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): clz.logger.debug_extra_verbose('readyToPlayQueue size:', self._ready_to_play_queue.qsize(), 'waited:', waited) return
def monitor(self): Monitor.throw_exception_if_abort_requested() if not self._monitor_thread or not self._monitor_thread.isAlive(): if self._monitor_thread: self._monitor_thread.join() self._monitor_thread = threading.Thread( target=self._monitor, name='AdvancedPlayer:MONITOR') self._monitor_thread.start()
def save_search_pages(self, flush=False): # type: (bool) -> None """ :return: """ with CacheIndex.lock: if (not flush and self.get_number_of_unsaved_changes() < Constants.TRAILER_CACHE_FLUSH_UPDATES and self.get_time_since_last_save() < datetime.timedelta(minutes=5)): return saved_pages = len(self._cached_page_by_key.items()) path = xbmcvfs.validatePath(self._path) temp_path = xbmcvfs.validatePath(self._temp_path) try: parent_dir, file_name = os.path.split(path) DiskUtils.create_path_if_needed(parent_dir) Monitor.throw_exception_if_abort_requested() with io.open(temp_path, mode='wt', newline=None, encoding='utf-8') as cacheFile: json_dict = self.to_json() # TODO: Need ability to interrupt when ABORT. Object_handler # not a valid arg to dumps json_text = json.dumps(json_dict, encoding='utf-8', ensure_ascii=False, default=CacheIndex.handler, indent=3, sort_keys=True) cacheFile.write(json_text) cacheFile.flush() self._number_of_unsaved_changes = 0 self._time_of_last_save = datetime.datetime.now() try: os.replace(temp_path, path) except OSError: self._logger.exception( f'Failed to replace move information' f' planned for download: {path}') Monitor.throw_exception_if_abort_requested() except AbortException: reraise(*sys.exc_info()) except IOError as e: self._logger.exception('') except JSONDecodeError as e: os.remove(path) except Exception as e: self._logger.exception('') self._logger.debug_verbose("Entries Saved: ", saved_pages)
def get_lock(cls, source: str): lock_source = cls.get_lock_source(source) if cls._logger.isEnabledFor(LazyLogger.DISABLED): cls._logger.debug_extra_verbose(f'Getting Lock: {lock_source} ' f'for {source}') while not cls.locks[source].acquire(blocking=False): Monitor.throw_exception_if_abort_requested(timeout=0.5) if cls._logger.isEnabledFor(LazyLogger.DISABLED): cls._logger.debug_extra_verbose(f'Got Lock: {source}')
def warning(self, line: str) -> None: if Monitor.is_abort_requested(): self._callback._error = 99 # Kill YoutubeDL Monitor.throw_exception_if_abort_requested() if 'merged' in line: # str: Requested formats are incompatible for merge and will be merged into # mkv. pass else: self.warning_lines.append(line)
def wait_if_too_many_requests(cls, source: str, video: bool) -> None: if source == Movie.ITUNES_SOURCE: return delay = cls.get_youtube_wait_seconds() if delay <= 0.0: return cls._logger.debug(f'Waiting for TOO_MANY_REQUESTS: {delay} seconds.') delay = cls.get_youtube_wait_seconds() if delay > 0.0: Monitor.throw_exception_if_abort_requested(delay) cls._logger.debug('Wait for TOO_MANY_REQUESTS complete')
def handler(obj: Any) -> Any: """ :param obj: :return: """ Monitor.throw_exception_if_abort_requested() if hasattr(obj, 'isoformat'): return obj.isoformat() else: raise TypeError( 'Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj)))
def read_cached_value_from_disk(cls): # type: () -> CacheParameters """ :return: """ path = os.path.join(Settings.get_remote_db_cache_path(), 'index', 'tmdb_discovery_parameters.json') path = xbmcvfs.validatePath(path) parent_dir, file_name = os.path.split(path) if not os.path.exists(parent_dir): DiskUtils.create_path_if_needed(parent_dir) saved_preferences = None with CacheIndex.lock: try: if not os.access(path, os.R_OK): cls._logger.error( Messages.get_formatted_msg(Messages.CAN_NOT_READ_FILE, path)) return None file_mod_time = datetime.datetime.fromtimestamp( os.path.getmtime(path)) now = datetime.datetime.now() expiration_time = now - datetime.timedelta( Settings.get_expire_remote_db_cache_entry_days()) if file_mod_time < expiration_time: if cls._logger.isEnabledFor(LazyLogger.DEBUG): cls._logger.debug('cache file EXPIRED for:', path) return None Monitor.throw_exception_if_abort_requested() with io.open(path, mode='rt', newline=None, encoding='utf-8') as cacheFile: saved_preferences = json.load(cacheFile, encoding='utf-8') saved_preferences = CacheParameters(saved_preferences) except AbortException: reraise(*sys.exc_info()) except IOError as e: cls._logger.exception('') exception_occurred = True except Exception as e: cls._logger.exception('') exception_occurred = True return saved_preferences
def unregister_slot(cls, signaler_id, signal): # type: (str, str) -> None """ :param signaler_id: :param signal: :return: """ Monitor.throw_exception_if_abort_requested() try: AddonSignals.unRegisterSlot(signaler_id, signal) except Exception: pass
def save_found_trailer_ids_cache(cls, flush=False): # type: (bool) -> None """ :param flush: :return: """ with cls.lock: if cls._unsaved_trailer_changes == 0: return if (not flush and (cls._unsaved_trailer_changes < Constants.TRAILER_CACHE_FLUSH_UPDATES) and (datetime.datetime.now() - cls._last_saved_trailer_timestamp) < datetime.timedelta(minutes=5)): return path = os.path.join(Settings.get_remote_db_cache_path(), 'index', 'tmdb_found_trailers.json') path = xbmcvfs.validatePath(path) parent_dir, file_name = os.path.split(path) if not os.path.exists(parent_dir): DiskUtils.create_path_if_needed(parent_dir) try: with io.open( path, mode='wt', newline=None, encoding='utf-8', ) as cacheFile: found_trailer_id_list = list( cls._found_tmdb_trailer_ids) json_text = json.dumps(found_trailer_id_list, encoding='utf-8', ensure_ascii=False, default=CacheIndex.handler, indent=3, sort_keys=True) cacheFile.write(json_text) cacheFile.flush() cls._last_saved_trailer_timestamp = datetime.datetime.now( ) cls._unsaved_trailer_changes = 0 Monitor.throw_exception_if_abort_requested() except AbortException: reraise(*sys.exc_info()) except IOError as e: CacheIndex.logger().exception('') except Exception as e: CacheIndex.logger().exception('')
def throw_exception_on_forced_to_stop(self, movie_data: AbstractMovieData = None, delay: float = 0) -> None: """ :param movie_data: :param delay: :return: """ Monitor.throw_exception_if_abort_requested(timeout=delay) if movie_data is not None and movie_data.restart_discovery_event.isSet( ): if self.logger.isEnabledFor(LazyLogger.DEBUG_VERBOSE): self.logger.debug_verbose('RestartDiscoveryException source:', movie_data.get_movie_source()) raise RestartDiscoveryException()
def shuffle_discovered_trailers(self, mark_unplayed=False): # type: (bool) -> None """ :param mark_unplayed: :return: """ clz = AbstractMovieData Monitor.throw_exception_if_abort_requested() # clz.logger.debug('before self.lock') with self._discovered_trailers_lock: # clz.logger.debug('Have discovered_trailers_lock') if self._discovered_trailers.len() == 0: return self._discovered_trailers.shuffle() if mark_unplayed: for trailer in self._discovered_trailers.get_trailers(): trailer[Movie.TRAILER_PLAYED] = False self._last_shuffled_index = self._discovered_trailers.len() - 1 self._last_shuffle_time = datetime.datetime.now() # Drain anything previously in queue self._discovered_trailers_queue.clear() Monitor.throw_exception_if_abort_requested() # clz.logger.debug('reloading _discovered_trailers_queue') for trailer in self._discovered_trailers.get_trailers(): if not trailer[Movie.TRAILER_PLAYED]: # if clz.logger.isEnabledFor(LazyLogger.DEBUG): # clz.logger.debug('adding', trailer[Movie.TITLE], # 'id:', hex(id(trailer)), # 'to discovered_trailers_queue', # 'state:', trailer[Movie.DISCOVERY_STATE]) self._discovered_trailers_queue.put(trailer) if clz.logger.isEnabledFor(LazyLogger.DISABLED): clz.logger.debug_extra_verbose( '_discoveredTrailerQueue length:', self._discovered_trailers_queue.qsize(), '_discovered_trailers length:', len(self._discovered_trailers))
def throw_exception_on_forced_to_stop(self, delay=0): # type: (float) -> None """ :param delay: :return: """ clz = BaseDiscoverMovies try: Monitor.throw_exception_if_abort_requested(timeout=delay) if self._movie_data.restart_discovery_event.isSet(): raise RestartDiscoveryException() except AbortException: if clz.logger.is_trace_enabled(Trace.TRACE_PLAY_STATS): PlayStatistics.report_play_count_stats() reraise(*sys.exc_info())
def register_slot(cls, signaler_id, signal, callback): # type: (str, str, Callable[[Any], None]) -> None """ :param signaler_id: :param signal: :param callback: :return: """ if PluginBridge._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): PluginBridge._logger.debug_extra_verbose('signaler_id:', signaler_id, 'signal:', signal, 'callback:', callback.__name__) Monitor.throw_exception_if_abort_requested() AddonSignals.registerSlot(signaler_id, signal, callback) PluginBridge._registered_slots.append((signaler_id, signal))
def get_next_trailer(cls): # type: () -> (Union[str, None], Union[Dict[str, Any], None]) """ front-end requests a trailer from the back-end and waits for response. :return: """ try: signal_payload = {} cls.send_signal('get_next_trailer', data=signal_payload, source_id=Constants.BACKEND_ID) # It can take some time before we get responses back # Wait max 30 seconds # TODO: handle case where there are NO trailers to play # Also, server should send ack on receipt of this request cls._status = FrontendBridgeStatus.BUSY count = 0 while cls._status == FrontendBridgeStatus.BUSY and count < 300: Monitor.throw_exception_if_abort_requested(timeout=0.10) count += 1 if count >= 300: cls._logger.error('Timed out waiting on get_next_trailer') cls._next_trailer = None cls._status = FrontendBridgeStatus.TIMED_OUT trailer = cls._next_trailer status = cls._status cls._next_trailer = None cls._status = FrontendBridgeStatus.IDLE if trailer is not None: if cls._logger.isEnabledFor(LazyLogger.DEBUG_EXTRA_VERBOSE): cls._logger.debug_extra_verbose('returning status:', status, 'title:', trailer[Movie.TITLE]) return status, trailer except AbortException: cls.delete_instance() reraise(*sys.exc_info()) except Exception as e: cls._logger.exception('')
def wrap_text(cls, movies_with_same_count: List[str], include_source: bool = False) -> None: """ :param logger: :param movies_with_same_count: :param include_source: :return: """ clz = PlayStatistics movies_in_line: List[str] = [] line_length = 3 # Initial spaces movie_title: str = '' for key in movies_with_same_count: Monitor.throw_exception_if_abort_requested() try: movie: Optional[MovieType] = cls._key_to_movie.get(key, None) if movie is not None: movie_title = movie.get(Movie.TITLE, 'unknown') else: movie_title = 'Not in dictionary' if include_source: movie_title = movie_title + f': {movie.get(Movie.SOURCE)}' if line_length + len(movie_title) >= clz.MAX_LINE_LENGTH: line = ' {}\n'.format(', '.join(movies_in_line)) cls.report.write(line) del movies_in_line[:] line_length = 3 # Initial spaces except AbortException: reraise(*sys.exc_info()) except Exception as e: clz.logger.exception() movies_in_line.append(movie_title) line_length += len(movie_title) + 2 # Slightly inaccurate if len(movies_in_line) > 0: try: line = ' {}\n'.format(', '.join(movies_in_line)) cls.report.write(line) except Exception as e: clz.logger.exception()