def can_user_connect(): # what better way to try than to just attempt to load the main menu? try: # TRY to use new method which is faster try: url = server + '/api/ping' ping = pyproxy.get_json(url, True) if ping is not None and b'pong' in ping: return True else: # should never happen return False except python_version_proxy.http_error as ex: # return false if it's an unauthorized response if ex.code == 401: return False eh.exception(ErrorPriority.NORMAL) # TODO DEPRECATE THIS AS MOST FUNCTIONS REQUIRE 3.9.5+ anyway # but since no one has it, we can't count on it actually working, so fall back from shoko_models.v2 import Filter f = Filter(0, build_full_object=True, get_children=False) if f.size < 1: raise RuntimeError(localized(30027)) return True except Exception as ex: xbmc.log(' ===== auth error ===== %s ' % ex, xbmc.LOGNOTICE) # because we always check for connection first, we can assume that auth is the only problem # we need to log in eh.exception(ErrorPriority.NORMAL) plugin_addon.setSetting('apikey', '') return False
def match_title(data, lang, title_type): try: exclude = False if title_type.startswith('!'): title_type = title_type[1:] exclude = True for title_tag in data.get('titles', []): title = pyproxy.decode(title_tag.get('Title', '')) if pyproxy.decode(title_tag.get('Title', '')) == '': continue if title_tag.get('Language', '').lower() != lang.lower(): continue # does it match the proper type if exclude and title_tag.get('Type', '').lower() == title_type.lower(): continue if not exclude and title_tag.get('Type', '').lower() != title_type.lower(): continue return title return None except: eh.exception(ErrorPriority.NORMAL) return None
def onPlayBackResumed(self): spam('Playback Resumed') self.PlaybackStatus = PlaybackStatus.PLAYING try: self.start_loops() except: eh.exception(ErrorPriority.HIGH)
def get_tags(tag_node): """ Get the tags from the new style Args: tag_node: node containing group Returns: a string of all of the tags formatted """ try: if tag_node is None: return '' if len(tag_node) == 0: return '' short_tag = plugin_addon.getSetting('short_tag_list') == 'true' temp_genres = [] current_length = 0 # the '3' here is because the separator ' | ' is 3 chars for tag in tag_node: if isinstance(tag, str) or isinstance(tag, unicode): if short_tag and current_length + len(tag) + 3 > 50: break temp_genres.append(tag) current_length += len(tag) + 3 else: temp_genre = pyproxy.decode(tag['tag']).strip() if short_tag and current_length + len(temp_genre) + 3 > 50: break temp_genres.append(temp_genre) current_length += len(temp_genre) + 3 return kproxy.parse_tags(temp_genres) except: eh.exception(ErrorPriority.NORMAL) return ''
def move_to_index(index, absolute=False): try: # putting this in a method crashes kodi to desktop. # region F**k if I know.... elapsed = 0 interval = 250 wait_time = 4000 control_list = None while True: if elapsed >= wait_time: break try: wind = xbmcgui.Window(xbmcgui.getCurrentWindowId()) control_list = wind.getControl(wind.getFocusId()) if isinstance(control_list, xbmcgui.ControlList): break except: pass xbmc.sleep(interval) elapsed += interval # endregion F**k if I know.... if isinstance(control_list, xbmcgui.ControlList): move_position_on_list(control_list, index, absolute) except: eh.exception(ErrorPriority.HIGH, localize2(30014))
def onPlayBackEnded(self): spam('Playback Ended') try: self.handle_finished_episode() except: eh.exception(ErrorPriority.HIGH) self.PlaybackStatus = PlaybackStatus.ENDED
def onPlayBackStarted(self): spam('Playback Started') try: if plugin_addon.getSetting('enableEigakan') == 'true': log('Player is set to use Transcoding') self.is_transcoded = True # wait until the player is init'd and playing self.set_duration() self.PlaybackStatus = PlaybackStatus.PLAYING # we are making the player global, so if a stop is issued, then Playing will change while not self.isPlaying( ) and self.PlaybackStatus == PlaybackStatus.PLAYING: xbmc.sleep(250) # TODO get series and populate info so we know if its movie or not # TODO maybe we could read trakt_id from shoko, self.is_movie = False if self.duration > 0 and self.scrobble: scrobble_trakt(self.ep_id, 1, self.getTime(), self.duration, self.is_movie) self.start_loops() except: eh.exception(ErrorPriority.HIGHEST)
def scrobble_time(self): if not self.scrobble: return try: scrobble_trakt(self.ep_id, 2, self.time, self.duration, self.is_movie) except: eh.exception(ErrorPriority.HIGH)
def post_data(self, url, data_in, custom_timeout=int(plugin_addon.getSetting('timeout'))): """ Send a message to the server and wait for a response Args: url: the URL to send the data to data_in: the message to send (in json) custom_timeout: if not given timeout from plugin setting will be used Returns: The response from the server """ import error_handler as eh from error_handler import ErrorPriority if data_in is None: data_in = b'' headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', } apikey = plugin_addon.getSetting('apikey') if apikey is not None and apikey != '': headers['apikey'] = apikey eh.spam('POSTing Data ---') eh.spam('URL:', url) eh.spam('Headers:', headers) eh.spam('POST Body:', data_in) try: # self.encode(url) # py3 fix req = Request(url, self.encode(data_in), headers) data_out = None response = urlopen(req, timeout=custom_timeout) data_out = response.read() response.close() eh.spam('Response Body:', data_out) eh.spam('Checking Response for a text error.\n') if data_out is not None and data_out != '': self.parse_possible_error(req, data_out) except timeout: # if using very short time out to not wait for response it will throw time out err, # but we check if that was intended by checking custom_timeout # if it wasn't intended we handle it the old way if custom_timeout == int(plugin_addon.getSetting('timeout')): eh.exception(ErrorPriority.HIGH) except http_error as err: raise err except Exception as ex: xbmc.log('==== post_data error ==== %s ' % ex, xbmc.LOGNOTICE) eh.exception(ErrorPriority.HIGH) return data_out
def kodi_jsonrpc(method, params): try: values = (pyproxy.decode(method), json.dumps(params)) request = '{"jsonrpc":"2.0","method":"%s","params":%s, "id": 1}' % values return_data = xbmc.executeJSONRPC(request) result = json.loads(return_data) return result except: eh.exception(ErrorPriority.HIGH, localize2(30016)) return None
def move_position_on_list_to_next(control_list): position = control_list.getSelectedPosition() if position != -1: try: control_list.selectItem(position+1) except: try: if position != 0: control_list.selectItem(position - 1) except: eh.exception(ErrorPriority.HIGH, localize2(30015))
def get_json(self, url_in, direct=False, force_cache=False, cache_time=0): """ use 'get' to return json body as string :param url_in: :param direct: force to bypass cache :param force_cache: force to use cache even if disabled :param cache_time: ignore setting to set custom cache expiration time, mainly to expire data quicker to refresh watch flags :return: """ import error_handler as eh from error_handler import ErrorPriority try: timeout = plugin_addon.getSetting('timeout') if self.api_key is None or self.api_key == '': apikey = plugin_addon.getSetting('apikey') else: apikey = self.api_key # if cache is disabled, overwrite argument and force it to direct if plugin_addon.getSetting('enableCache') != 'true': direct = True if direct and not force_cache: body = self.get_data(url_in, None, timeout, apikey) else: import cache eh.spam('Getting a Cached Response ---') eh.spam('URL:', url_in) db_row = cache.get_data_from_cache(url_in) if db_row is not None: valid_until = cache_time if cache_time > 0 else int( plugin_addon.getSetting('expireCache')) expire_second = time.time() - float(db_row[1]) if expire_second > valid_until: # expire, get new date eh.spam('The cached data is stale.') body = self.get_data(url_in, None, timeout, apikey) cache.remove_cache(url_in) cache.add_cache(url_in, body) else: body = db_row[0] else: eh.spam('No cached data was found for the URL.') body = self.get_data(url_in, None, timeout, apikey) cache.add_cache(url_in, body) except http_error as err: raise err except Exception as ex: xbmc.log(' ========= ERROR JSON ============ %s' % ex, xbmc.LOGNOTICE) eh.exception(ErrorPriority.HIGH) body = None return body
def get_kodi_setting(setting): try: if setting in kodi_settings_cache: return kodi_settings_cache[setting] method = 'Settings.GetSettingValue' params = {'setting': setting} result = kodi_jsonrpc(method, params) if result is not None and 'result' in result and 'value' in result['result']: result = result['result']['value'] kodi_settings_cache[setting] = result return result except: eh.exception(ErrorPriority.HIGH) return None
def onAVStarted(self): # Will be called when Kodi has a video or audiostream, before playing file spam('onAVStarted') # isExternalPlayer() ONLY works when isPlaying(), other than that it throw 0 always # setting it before results in false setting try: is_external = str(kodi_proxy.external_player(self)).lower() plugin_addon.setSetting(id='external_player', value=is_external) except: eh.exception(ErrorPriority.HIGH) spam(self) if kodi_proxy.external_player(self): log('Using External Player') self.is_external = True
def debug_init(): """ start debugger if it's enabled also dump argv if spamLog :return: """ if plugin_addon.getSetting('remote_debug') == 'true': # try pycharm first try: import pydevd # try to connect multiple times...in case we forgot to start it # TODO Show a message to the user that we are waiting on the debugger connected = False tries = 0 while not connected and tries < 60: try: pydevd.settrace(host=plugin_addon.getSetting('remote_ip'), stdoutToServer=True, stderrToServer=True, port=5678, suspend=False) eh.spam('Connected to debugger') connected = True except: tries += 1 # we keep this message the same, as kodi will merge them into Previous line repeats... eh.spam('Failed to connect to debugger') xbmc.sleep(1000) except (ImportError, NameError): eh.log( 'unable to import pycharm debugger, falling back on the web-pdb' ) try: import web_pdb web_pdb.set_trace() except Exception: eh.exception(ErrorPriority.NORMAL, 'Unable to start debugger, disabling it') plugin_addon.setSetting('remote_debug', 'false') except: eh.exception(ErrorPriority.HIGHEST, 'Unable to start debugger') eh.spam('argv:', sys.argv)
def get_title(data, lang=None, title_type=None): """ Get the title based on settings :param data: json node containing the title :return: string of the desired title :rtype: str """ try: if 'titles' not in data or plugin_addon.getSetting( 'use_server_title') == 'true': return pyproxy.decode(data.get('name', '')) # xbmc.log(data.get('title', 'Unknown')) title = pyproxy.decode(data.get('name', '').lower()) if is_type_list(title): return pyproxy.decode(data.get('name', '')) if lang is None: lang = plugin_addon.getSetting('displaylang') if title_type is None: title_type = plugin_addon.getSetting('title_type') # try to match title = match_title(data, lang, title_type) if title is not None: return title # fallback on any type of same language title = match_title(data, lang, '!short') if title is not None: return title # fallback on x-jat main title title = match_title(data, 'x-jat', 'main') if title is not None: return title # fallback on directory title return pyproxy.decode(data.get('name', '')) except: eh.exception(ErrorPriority.NORMAL) return 'util.error'
def set_window_heading(category, window_name): """ Sets the window titles Args: category: Primary name window_name: Secondary name """ handle = int(sys.argv[1]) xbmcplugin.setPluginCategory(handle, category) window_obj = xbmcgui.Window(xbmcgui.getCurrentWindowId()) try: window_obj.setProperty('heading', str(window_name)) except Exception as e: eh.exception(ErrorPriority.LOW, localize2(30013)) window_obj.clearProperty('heading') try: window_obj.setProperty('heading2', str(window_name)) except Exception as e: eh.exception(ErrorPriority.LOW, localize2(30013) + ' 2') window_obj.clearProperty('heading2')
def move_position_on_list(control_list, position=0, absolute=False): """ Move to the position in a list - use episode number for position Args: control_list: the list control position: the move_position_on_listindex of the item not including settings absolute: bypass setting and set position directly """ if not absolute: if position < 0: position = 0 if plugin_addon.getSetting('show_continue') == 'true': position = int(position + 1) if get_kodi_setting('filelists.showparentdiritems'): position = int(position + 1) try: control_list.selectItem(position) xbmc.log(' move_position_on_list : %s ' % position, xbmc.LOGNOTICE) except: try: control_list.selectItem(position - 1) except Exception as e: xbmc.log(' -----> ERROR -----> %s' % e, xbmc.LOGNOTICE) eh.exception(ErrorPriority.HIGH, localize2(30015))
def vote(self, vote_type): if self.dbid == '': eh.exception(eh.ErrorPriority.HIGHEST, 'Unable to Vote for Series', 'No ID was found on the object') return if vote_type == 'series': try: self._vote_series() except: eh.exception(eh.ErrorPriority.HIGHEST) elif vote_type == self.media_type: try: self._vote_episode() except: eh.exception(eh.ErrorPriority.HIGHEST)
def get_server_status(ip=plugin_addon.getSetting('ipaddress'), port=plugin_addon.getSetting('port')): """ Try to query server for status, display messages as needed don't bother with caching, this endpoint is really fast :return: bool """ if port is None: port = plugin_addon.getSetting('port') if isinstance(port, basestring): port = pyproxy.safe_int(port) port = port if port != 0 else 8111 url = 'http://%s:%i/api/init/status' % (ip, port) try: # this should throw if there's an error code response = pyproxy.get_json(url, True) # we should have a json response now # example: # {"startup_state":"Complete!","server_started":false,"server_uptime":"04:00:45","first_run":false,"startup_failed":false,"startup_failed_error_message":""} json_tree = json.loads(response) server_started = json_tree.get('server_started', False) startup_failed = json_tree.get('startup_failed', False) startup_state = json_tree.get('startup_state', '') # server started successfully if server_started: return True # not started successfully if startup_failed: # server finished trying to start, but failed message_box(localized(30017), localized(30018), localized(30019), localized(30020)) return False was_canceled = False busy = xbmcgui.DialogProgress() busy.create(localized(30021), startup_state) busy.update(1) # poll every second until the server gives us a response that we want while True: xbmc.sleep(1000) response = pyproxy.get_json(url, True) # this should not happen if response is None or pyproxy.safe_int(response) > 200: busy.close() message_box(localized(30022), localized(30023), localized(30033), localized(30034)) return False json_tree = json.loads(response) server_started = json_tree.get('server_started', False) if server_started: busy.close() return True startup_failed = json_tree.get('startup_failed', False) if json_tree.get('startup_state', '') == startup_state: continue startup_state = json_tree.get('startup_state', '') busy.update(1, localized(30021), startup_state) if startup_failed: break if busy.iscanceled(): was_canceled = True break busy.close() if was_canceled: return False if startup_failed: message_box(localized(30017), localized(30018), localized(30019), localized(30020)) return False return True except python_version_proxy.http_error as httperror: eh.exception(ErrorPriority.NORMAL) if httperror.code == 503: return startup_handle_no_connection(ip, port) if httperror.code == 404: return startup_handle_404() show_connection_error() return False except: eh.exception(ErrorPriority.HIGHEST) return False
def player_loop(player, is_transcoded, is_transcode_finished, ep_id, party_mode): try: monitor = xbmc.Monitor() # seek to beginning of stream :hack: https://github.com/peak3d/inputstream.adaptive/issues/94 if is_transcoded: while not xbmc.Player().isPlayingVideo(): monitor.waitForAbort(0.25) if not is_transcode_finished: if xbmc.Player().isPlayingVideo(): log('Seek back - so the stream is from beginning') # TODO part1: hack is temporary and not working in 100% # TODO part2: (with small segments + fast cpu, you wont start from 1st segment) #xbmc.executebuiltin('Seek(-60)') xbmc.executeJSONRPC( '{"jsonrpc":"2.0","method":"Player.Seek","params":{"playerid":1,"value":{"seconds":0}},"id":1}' ) while player.PlaybackStatus != PlaybackStatus.STOPPED and player.PlaybackStatus != PlaybackStatus.ENDED: xbmc.sleep(500) if player.PlaybackStatus == PlaybackStatus.STOPPED or player.PlaybackStatus == PlaybackStatus.ENDED: log('Playback Ended - Shutting Down: ', monitor.abortRequested()) if player.is_finished: log('post-finish: start events') if ep_id != 0: from shoko_models.v2 import Episode ep = Episode(ep_id, build_full_object=False) spam('mark as watched, episode') ep.set_watched_status(True) # wait till directory is loaded while kodi_utils.is_dialog_active(): xbmc.sleep(500) # refresh it, so it moves onto next item and the mark watched is refreshed kodi_utils.refresh() # wait till it load again while kodi_utils.is_dialog_active(): xbmc.sleep(500) if int(ep_id) != 0 and plugin_addon.getSetting( 'vote_always') == 'true' and not party_mode: spam('vote_always, voting on episode') script_utils.vote_for_episode(ep_id) if int(ep_id) != 0 and plugin_addon.getSetting( 'vote_on_series') == 'true' and not party_mode: from shoko_models.v2 import get_series_for_episode series = get_series_for_episode(ep_id) # voting should be only when you really watch full series spam('vote_on_series, mark: %s / %s' % (series.sizes.watched_episodes, series.sizes.total_episodes)) if series.sizes.watched_episodes - series.sizes.total_episodes == 0: script_utils.vote_for_series(series.id) return -1 else: log( 'Playback Ended - Playback status was not "Stopped" or "Ended". It was ', player.PlaybackStatus) return 0 except: eh.exception(ErrorPriority.HIGHEST) return -1
def play_video(file_id, ep_id=0, mark_as_watched=True, resume=False, force_direct_play=False, force_transcode_play=False, party_mode=False): """ Plays a file :param file_id: file ID. It is needed to look up the file :param ep_id: episode ID, not needed, but it fills in a lot of info :param mark_as_watched: should we mark it after playback :param resume: should we auto-resume :param force_direct_play: force direct play :param force_transcode_play: force transcoding file :return: True if successfully playing """ eh.spam('Processing play_video %s %s %s %s %s %s' % (file_id, ep_id, mark_as_watched, resume, force_direct_play, force_transcode_play)) from shoko_models.v2 import Episode, File, get_series_for_episode # check if we're already playing something player = xbmc.Player() if player.isPlayingVideo(): playing_item = player.getPlayingFile() log('Player is currently playing %s' % playing_item) log('Player Stopping') player.stop() # wait for it to stop while True: try: if not player.isPlayingVideo(): break xbmc.sleep(500) continue except: pass # now continue file_url = '' if int(ep_id) != 0: ep = Episode(ep_id, build_full_object=True) series = get_series_for_episode(ep_id) ep.series_id = series.id ep.series_name = series.name item = ep.get_listitem() f = ep.get_file_with_id(file_id) else: f = File(file_id, build_full_object=True) item = f.get_listitem() if item is not None: if resume: # TODO looks like this does nothing... item.resume() else: item.setProperty('ResumeTime', '0') file_url = f.url_for_player if f is not None else None if file_url is not None: is_transcoded = False m3u8_url = '' subs_extension = '' is_finished = False if not force_direct_play: if 'smb://' in file_url: file_url = f.remote_url_for_player is_transcoded, m3u8_url, subs_extension, is_finished = process_transcoder( file_id, file_url, force_transcode_play) player = Player() player.feed(file_id, ep_id, f.duration, m3u8_url if is_transcoded else file_url, mark_as_watched) try: item.setProperty('IsPlayable', 'true') if is_transcoded: #player.play(item=m3u8_url) url_for_player = m3u8_url item.setPath(url_for_player) item.setProperty('inputstreamaddon', 'inputstream.adaptive') item.setProperty('inputstream.adaptive.manifest_type', 'mpd') item.setMimeType('application/dash+xml') item.setContentLookup(False) # TODO maybe extract all subs and include them ? subs_url = eigakan_host + '/api/video/%s/%s/subs.%s' % ( clientid, file_id, subs_extension) if pyproxy.head(url_in=subs_url): item.setSubtitles([ subs_url, ]) item.addStreamInfo('subtitle', { 'language': 'Default', }) else: #file_url = f.remote_url_for_player #player.play(item=file_url, listitem=item) url_for_player = f.url_for_player # file_url item.setPath(url_for_player) handle = int(sys.argv[1]) if handle == -1: player.play(item=url_for_player, listitem=item) else: # thanks to anxdpanic for pointing in right direction xbmcplugin.setResolvedUrl(handle, True, item) except: eh.exception(ErrorPriority.BLOCKING) # leave player alive so we can handle onPlayBackStopped/onPlayBackEnded # TODO Move the instance to Service, so that it is never disposed xbmc.sleep(int(plugin_addon.getSetting('player_sleep'))) return player_loop(player, is_transcoded, is_finished, ep_id, party_mode)
def process_transcoder(file_id, file_url, force_transcode_play=False): """ :param file_id: :param file_url: :param force_transcode_play: force transcode :return: """ is_transcoded = False m3u8_url = '' subs_type = '' is_finished = False if plugin_addon.getSetting( 'enableEigakan') != 'true' and not force_transcode_play: return is_transcoded, m3u8_url, subs_type, is_finished video_url = trancode_url(file_id) post_data = '"file":"' + file_url + '"' is_dash = True end_url = eigakan_host + '/api/video/%s/%s/end.eigakan' % (clientid, file_id) if is_dash: m3u8_url = eigakan_host + '/api/video/%s/%s/play.mpd' % (clientid, file_id) ts_url = eigakan_host + '/api/video/%s/%s/%s' % (clientid, file_id, magic_chunk) else: m3u8_url = eigakan_host + '/api/video/%s/%s/play.m3u8' % (clientid, file_id) ts_url = eigakan_host + '/api/video/%s/%s/play0.ts' % (clientid, file_id) try: kodi_utils.check_eigakan() # server is alive so send profile of device we didn't before if plugin_addon.getSetting('eigakan_handshake') == 'false': kodi_utils.send_profile() settings = get_client_settings() # check if file is already transcoded is_finished = pyproxy.head(url_in=end_url) if not is_finished: # let's probe file, maybe we already know which streams we want busy.create(plugin_addon.getLocalizedString(30160), plugin_addon.getLocalizedString(30177)) audio_streams, subs_streams = eigakan_utils.probe_file( file_id, file_url) busy.close() # pick streams that are preferred via profile on eigakan a_index, s_index, subs_type = eigakan_utils.pick_best_streams( audio_streams, subs_streams) # region BUSY Dialog Hell # please wait, Sending request to Transcode server... busy.create(plugin_addon.getLocalizedString(30160), plugin_addon.getLocalizedString(30165)) if a_index > -1: post_data += ',"audio":"%s"' % a_index if s_index > -1: post_data += ',"subtitles":"%s"' % s_index pyproxy.post_json(video_url, post_data, custom_timeout=0.1) # non blocking xbmc.sleep(1000) # busy.close() try_count = 0 found = False # please wait,waiting for being queued busy.update(0, plugin_addon.getLocalizedString(30192)) while True: if busy.iscanceled(): break if eigakan_utils.is_fileid_added_to_transcoder(file_id): break try_count += 1 busy.update(try_count) xbmc.sleep(1000) try_count = 0 found = False # plase wait, waiting for subs to be dump busy.update(try_count, plugin_addon.getLocalizedString(30205)) while True: if busy.iscanceled(): break ask_for_subs = json.loads( pyproxy.get_json(eigakan_host + '/api/queue/%s' % file_id)) if ask_for_subs is None: ask_for_subs = {} y = ask_for_subs.get('queue', {"videos": {}}).get('videos', {}) for k in y: if int(k) == int(file_id): found = True break if found: break if found: break try_count += 1 if try_count >= 100: try_count = 0 busy.update(try_count, plugin_addon.getLocalizedString(30218)) busy.update(try_count) xbmc.sleep(1000) try_count = 0 found = False # please waiting, waiting for starting transcode busy.update(try_count, plugin_addon.getLocalizedString(30206)) while True: if busy.iscanceled(): break ask_for_subs = json.loads( pyproxy.get_json(eigakan_host + '/api/queue/%s' % file_id)) if ask_for_subs is None: ask_for_subs = {} x = ask_for_subs.get('queue', {"videos": {}}).get('videos', {}) for k in x: if int(k) == int(file_id): percent = x[k].get('percent', 0) if int(percent) > 0: found = True log('percent found of transcoding: %s' % percent) break if found: break try_count += 1 if try_count >= 100: try_count = 0 busy.update(try_count, plugin_addon.getLocalizedString(30218)) busy.update(try_count) xbmc.sleep(1000) try_count = 0 # please wait, Waiting for response from Server... busy.update(try_count, plugin_addon.getLocalizedString(30164)) while True: if busy.iscanceled(): break if pyproxy.head(url_in=ts_url) is False: try_count += 1 busy.update(try_count) xbmc.sleep(1000) else: break busy.close() # endregion if pyproxy.head(url_in=ts_url): is_transcoded = True except: eh.exception(ErrorPriority.BLOCKING) try: busy.close() except: pass return is_transcoded, m3u8_url, subs_type, is_finished