def only_Shanka(self): '''Denote the Shankarappa region. Returns: index: boolean index with the Shankarappa region marked by 'True's. *Note*: The region is marked by 'EEE' at the beginning and 'RPGGG' at the end. ''' cons = self.get_consensus_initial() is_Shanka = np.zeros(len(cons), bool) consaa = ''.join(translate(cons)) is_Shanka[consaa.find(self.aaS[0]) * 3:consaa.find(self.aaS[1]) * 3] = True return is_Shanka
def boxsets(self, library_id=None, dialog=None): ''' Process all boxsets. ''' Movies = self.library.media['Movies'] for items in server.get_items(library_id, "BoxSet", False, self.sync['RestorePoint'].get('params')): with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, boxset in enumerate(items['Items']): dialog.update( int((float(start_index + index) / float(items['TotalRecordCount'])) * 100), heading="%s: %s" % (translate('addon_name'), translate('boxsets')), message=boxset['Name']) obj.boxset(boxset)
def service(self): ''' Keeps the service monitor going. Exit on Kodi shutdown or profile switch. if profile switch happens more than once, Threads depending on abortRequest will not trigger. ''' self.monitor = monitor.Monitor() player = self.monitor.player self.connect = connect.Connect() self.start_default() self.settings['mode'] = settings('useDirectPaths') while self.running: if window('jellyfin_online.bool'): if self.settings['profile'] != window('jellyfin_kodiProfile'): LOG.info("[ profile switch ] %s", self.settings['profile']) break if player.isPlaying() and player.is_playing_file(player.get_playing_file()): difference = datetime.today() - self.settings['last_progress'] if difference.seconds > 10: self.settings['last_progress'] = datetime.today() update = (datetime.today() - self.settings['last_progress_report']).seconds > 250 event('ReportProgressRequested', {'Report': update}) if update: self.settings['last_progress_report'] = datetime.today() if window('jellyfin.restart.bool'): window('jellyfin.restart', clear=True) dialog("notification", heading="{jellyfin}", message=translate(33193), icon="{jellyfin}", time=1000, sound=False) raise Exception('RestartService') if self.waitForAbort(1): break self.shutdown() raise Exception("ExitService")
def window_node(self, index, view, node=None, node_label=None): ''' Leads to another listing of nodes. ''' if view['Media'] in ('homevideos', 'photos'): path = self.window_browse( view, None if node in ('all', 'browse') else node) elif node == 'nextepisodes': path = self.window_nextepisodes(view) elif node == 'music': path = self.window_music(view) elif node == 'browse': path = self.window_browse(view) else: path = self.window_path(view, node) if node == 'music': window_path = "ActivateWindow(Music,%s,return)" % path elif node in ('browse', 'homevideos', 'photos'): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type( node_label) == int else node_label node_label = node_label or view['Name'] if node in ('all', 'music'): window_prop = "Jellyfin.nodes.%s" % index window('%s.index' % window_prop, path.replace('all.xml', "")) # dir window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) elif node == 'browse': window_prop = "Jellyfin.nodes.%s" % index window('%s.title' % window_prop, view['Name']) else: window_prop = "Jellyfin.nodes.%s.%s" % (index, node) window('%s.title' % window_prop, node_label) window('%s.content' % window_prop, path) window('%s.id' % window_prop, view['Id']) window('%s.path' % window_prop, window_path) window('%s.type' % window_prop, view['Media']) self.window_artwork(window_prop, view['Id'])
def manage_libraries(): directory(translate(33098), "plugin://plugin.video.jellyfin/?mode=refreshboxsets", False) directory(translate(33154), "plugin://plugin.video.jellyfin/?mode=addlibs", False) directory(translate(33139), "plugin://plugin.video.jellyfin/?mode=updatelibs", False) directory(translate(33140), "plugin://plugin.video.jellyfin/?mode=repairlibs", False) directory(translate(33184), "plugin://plugin.video.jellyfin/?mode=removelibs", False) directory(translate(33060), "plugin://plugin.video.jellyfin/?mode=thememedia", False) xbmcplugin.setContent(PROCESS_HANDLE, 'files') xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def window_wnode(self, index, view, node=None, node_label=None): ''' Similar to window_node, but does not contain music, musicvideos. Contains books, audiobooks. ''' if view['Media'] in ('homevideos', 'photos', 'books', 'playlists'): path = self.window_browse( view, None if node in ('all', 'browse') else node) else: path = self.window_path(view, node) if node in ('browse', 'homevideos', 'photos', 'books', 'playlists'): window_path = path else: window_path = "ActivateWindow(Videos,%s,return)" % path node_label = translate(node_label) if type( node_label) == int else node_label node_label = node_label or view['Name'] if node == 'all': window_prop = "Jellyfin.wnodes.%s" % index window('%s.index' % window_prop, path.replace('all.xml', "")) # dir window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) elif node == 'browse': window_prop = "Jellyfin.wnodes.%s" % index window('%s.title' % window_prop, view['Name']) window('%s.content' % window_prop, path) else: window_prop = "Jellyfin.wnodes.%s.%s" % (index, node) window('%s.title' % window_prop, node_label) window('%s.content' % window_prop, path) window('%s.id' % window_prop, view['Id']) window('%s.path' % window_prop, window_path) window('%s.type' % window_prop, view['Media']) self.window_artwork(window_prop, view['Id']) LOG.debug("--[ wnode/%s/%s ] %s", index, window('%s.title' % window_prop), window('%s.artwork' % window_prop))
def tvshows(self, library, dialog): ''' Process tvshows and episodes from a single library. ''' TVShows = self.library.media['TVShows'] with self.library.database_lock: with Database() as videodb: with Database('jellyfin') as jellyfindb: obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, True) for items in server.get_items( library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params'][ 'StartIndex'] for index, show in enumerate(items['Items']): percent = int( (float(start_index + index) / float(items['TotalRecordCount'])) * 100) message = show['Name'] dialog.update( percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message) if obj.tvshow(show, library=library) is not False: for episodes in server.get_episode_by_show( show['Id']): for episode in episodes['Items']: dialog.update( percent, message="%s/%s" % (message, episode['Name'][:10])) obj.episode(episode) if self.update_library: self.tvshows_compare(library, obj, jellyfindb)
def onClick(self, control): if control == CONNECT: self._disable_error() server = self.host_field.getText() if not server: # Display error self._error(ERROR['Empty'], translate('empty_server')) LOG.error("Server cannot be null") elif self._connect_to_server(server): self.close() elif control == CANCEL: # Remind me later self.close()
def node_index(self, folder, view, mixed=False): file = os.path.join(folder, "index.xml") index = self.sync['SortedViews'].index(view['Id']) try: xml = etree.parse(file).getroot() xml.set('order', str(index)) except Exception as error: LOG.exception(error) xml = self.node_root('main', index) etree.SubElement(xml, 'label') label = xml.find('label') label.text = view['Name'] if not mixed else "%s (%s)" % ( view['Name'], translate(view['Media'])) indent(xml) write_xml(etree.tostring(xml, 'UTF-8'), file)
def onClick(self, control): if control == SIGN_IN: self._disable_error() user = self.user_field.getText() password = self.password_field.getText() if not user: # Display error self._error(ERROR['Empty'], translate('empty_user')) LOG.error("Username cannot be null") elif self._login(user, password): self.close() elif control == CANCEL: # Remind me later self.close()
def music(self, library, dialog): ''' Process artists, album, songs from a single library. ''' Music = self.library.media['Music'] with self.library.music_database_lock: with Database('music') as musicdb: with Database('jellyfin') as jellyfindb: obj = Music(self.server, jellyfindb, musicdb, self.direct_path) for items in server.get_artists(library['Id'], False, self.sync['RestorePoint'].get('params')): self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, artist in enumerate(items['Items']): percent = int((float(start_index + index) / float(items['TotalRecordCount'])) * 100) message = artist['Name'] dialog.update(percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message) obj.artist(artist, library=library) for albums in server.get_albums_by_artist(artist['Id']): for album in albums['Items']: obj.album(album) for songs in server.get_items(album['Id'], "Audio"): for song in songs['Items']: dialog.update(percent, message="%s/%s/%s" % (message, album['Name'][:7], song['Name'][:7])) obj.song(song) for songs in server.get_songs_by_artist(artist['Id']): for song in songs['Items']: dialog.update(percent, message="%s/%s" % (message, song['Name'])) obj.song(song) if self.update_library: self.music_compare(library, obj, jellyfindb)
def _set_intros(self, item): ''' if we have any play them when the movie/show is not being resumed. ''' intros = TheVoid('GetIntros', { 'ServerId': self.server_id, 'Id': item['Id'] }).get() if intros['Items']: enabled = True if settings('askCinema') == "true": resp = dialog("yesno", heading="{jellyfin}", line1=translate(33016)) if not resp: enabled = False LOG.info("Skip trailers.") if enabled: for intro in intros['Items']: listitem = xbmcgui.ListItem() LOG.info("[ intro/%s ] %s", intro['Id'], intro['Name']) play = playutils.PlayUtils(intro, False, self.server_id, self.server) source = play.select_source(play.get_sources()) self.set_listitem(intro, listitem, intro=True) listitem.setPath(intro['PlaybackInfo']['Path']) playutils.set_properties(intro, intro['PlaybackInfo']['Method'], self.server_id) self.stack.append( [intro['PlaybackInfo']['Path'], listitem]) window('jellyfin.skip.%s' % intro['Id'], value="true")
def run(self): while True: try: item = self.queue.get(timeout=3) except Queue.Empty: break time = self.music_time if item[0] == 'Audio' else self.video_time if time and (not self.player.isPlayingVideo() or xbmc.getCondVisibility('VideoPlayer.Content(livetv)')): dialog("notification", heading="%s %s" % (translate(33049), item[0]), message=item[1], icon="{jellyfin}", time=time, sound=False) self.queue.task_done() if window('jellyfin_should_stop.bool'): break LOG.info("--<[ q:notify/%s ]", id(self)) self.is_done = True
def set_web_server(self): ''' Enable the webserver if not enabled. This is used to cache artwork. Will only test once, if it fails, user will be notified only once. ''' if settings('enableTextureCache.bool'): get_setting = JSONRPC('Settings.GetSettingValue') if not self.get_web_server(): set_setting = JSONRPC('Settings.SetSetingValue') set_setting.execute({ 'setting': "services.webserverport", 'value': 8080 }) set_setting.execute({ 'setting': "services.webserver", 'value': True }) if not self.get_web_server(): settings('enableTextureCache.bool', False) dialog("ok", heading="{jellyfin}", line1=translate(33103)) return result = get_setting.execute({'setting': "services.webserverport"}) settings('webServerPort', str(result['result']['value'] or "")) result = get_setting.execute( {'setting': "services.webserverusername"}) settings('webServerUser', str(result['result']['value'] or "")) result = get_setting.execute( {'setting': "services.webserverpassword"}) settings('webServerPass', str(result['result']['value'] or "")) settings('useWebServer.bool', True)
def add_user(api_client): ''' Add or remove users from the default server session. ''' if not window('jellyfin_online.bool'): return session = api_client.get_device(client.get_device_id()) users = api_client.get_users() current = session[0]['AdditionalUsers'] result = dialog("select", translate(33061), [translate(33062), translate(33063)] if current else [translate(33062)]) if result < 0: return if not result: # Add user eligible = [ x for x in users if x['Id'] not in [current_user['UserId'] for current_user in current] ] resp = dialog("select", translate(33064), [x['Name'] for x in eligible]) if resp < 0: return user = eligible[resp] event('AddUser', {'Id': user['Id'], 'Add': True}) else: # Remove user resp = dialog("select", translate(33064), [x['UserName'] for x in current]) if resp < 0: return user = current[resp] event('AddUser', {'Id': user['UserId'], 'Add': False})
def browse(media, view_id=None, folder=None, server_id=None, api_client=None): ''' Browse content dynamically. ''' LOG.info("--[ v:%s/%s ] %s", view_id, media, folder) if not window('jellyfin_online.bool') and server_id is None: monitor = xbmc.Monitor() for i in range(300): if window('jellyfin_online.bool'): break elif monitor.waitForAbort(0.1): return else: LOG.error("Default server is not online.") return folder = folder.lower() if folder else None if folder is None and media in ('homevideos', 'movies', 'books', 'audiobooks'): return browse_subfolders(media, view_id, server_id) if folder and folder == 'firstletter': return browse_letters(media, view_id, server_id) if view_id: view = api_client.get_item(view_id) xbmcplugin.setPluginCategory(PROCESS_HANDLE, view['Name']) content_type = "files" if media in ('tvshows', 'seasons', 'episodes', 'movies', 'musicvideos', 'songs', 'albums'): content_type = media elif media in ('homevideos', 'photos'): content_type = "images" elif media in ('books', 'audiobooks'): content_type = "videos" elif media == 'music': content_type = "artists" if folder == 'recentlyadded': listing = api_client.get_recently_added(None, view_id, None) elif folder == 'genres': listing = api_client.get_genres(view_id) elif media == 'livetv': listing = api_client.get_channels() elif folder == 'unwatched': listing = get_filtered_section(view_id, None, None, None, None, None, ['IsUnplayed'], None, server_id, api_client) elif folder == 'favorite': listing = get_filtered_section(view_id, None, None, None, None, None, ['IsFavorite'], None, server_id, api_client) elif folder == 'inprogress': listing = get_filtered_section(view_id, None, None, None, None, None, ['IsResumable'], None, server_id, api_client) elif folder == 'boxsets': listing = get_filtered_section(view_id, get_media_type('boxsets'), None, True, None, None, None, None, server_id, api_client) elif folder == 'random': listing = get_filtered_section(view_id, get_media_type(content_type), 25, True, "Random", None, None, None, server_id, api_client) elif (folder or "").startswith('firstletter-'): listing = get_filtered_section( view_id, get_media_type(content_type), None, None, None, None, None, {'NameStartsWith': folder.split('-')[1]}, server_id, api_client) elif (folder or "").startswith('genres-'): listing = get_filtered_section(view_id, get_media_type(content_type), None, None, None, None, None, {'GenreIds': folder.split('-')[1]}, server_id, api_client) elif folder == 'favepisodes': listing = get_filtered_section(None, get_media_type(content_type), 25, None, None, None, ['IsFavorite'], None, server_id, api_client) elif folder and media == 'playlists': listing = get_filtered_section(folder, get_media_type(content_type), None, False, 'None', None, None, None, server_id, api_client) elif media == 'homevideos': listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, False, None, None, None, None, server_id, api_client) elif media in ['movies', 'episodes']: listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, True, None, None, None, None, server_id, api_client) elif media in ('boxset', 'library'): listing = get_filtered_section(folder or view_id, None, None, True, None, None, None, None, server_id, api_client) elif media == 'boxsets': listing = get_filtered_section(folder or view_id, None, None, False, None, None, ['Boxsets'], None, server_id, api_client) elif media == 'tvshows': listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, True, None, None, None, None, server_id, api_client) elif media == 'seasons': listing = api_client.get_seasons(folder) elif media != 'files': listing = get_filtered_section(folder or view_id, get_media_type(content_type), None, False, None, None, None, None, server_id, api_client) else: listing = get_filtered_section(folder or view_id, None, None, False, None, None, None, None, server_id, api_client) if listing: actions = Actions(server_id, api_client) list_li = [] listing = listing if type(listing) == list else listing.get( 'Items', []) for item in listing: li = xbmcgui.ListItem() li.setProperty('jellyfinid', item['Id']) li.setProperty('jellyfinserver', server_id) actions.set_listitem(item, li) if item.get('IsFolder'): params = { 'id': view_id or item['Id'], 'mode': "browse", 'type': get_folder_type(item, media) or media, 'folder': item['Id'], 'server': server_id } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) context = [] if item['Type'] in ('Series', 'Season', 'Playlist'): context.append(( "Play", "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))) if item['UserData']['Played']: context.append((translate( 16104 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) else: context.append((translate( 16103 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) li.addContextMenuItems(context) list_li.append((path, li, True)) elif item['Type'] == 'Genre': params = { 'id': view_id or item['Id'], 'mode': "browse", 'type': get_folder_type(item, media) or media, 'folder': 'genres-%s' % item['Id'], 'server': server_id } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) list_li.append((path, li, True)) else: if item['Type'] not in ('Photo', 'PhotoAlbum'): params = { 'id': item['Id'], 'mode': "play", 'server': server_id } path = "%s?%s" % ("plugin://plugin.video.jellyfin/", urlencode(params)) li.setProperty('path', path) context = [(translate( 13412 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=playlist&id=%s&server=%s)" % (item['Id'], server_id))] if item['UserData']['Played']: context.append((translate( 16104 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=unwatched&id=%s&server=%s)" % (item['Id'], server_id))) else: context.append((translate( 16103 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=watched&id=%s&server=%s)" % (item['Id'], server_id))) li.addContextMenuItems(context) list_li.append((li.getProperty('path'), li, False)) xbmcplugin.addDirectoryItems(PROCESS_HANDLE, list_li, len(list_li)) if content_type == 'images': xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_TITLE) xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_DATE) xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RATING) xbmcplugin.addSortMethod(PROCESS_HANDLE, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) xbmcplugin.setContent(PROCESS_HANDLE, content_type) xbmcplugin.endOfDirectory(PROCESS_HANDLE)
def fast_sync(self): ''' Movie and userdata not provided by server yet. ''' last_sync = settings('LastIncrementalSync') include = [] filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] sync = get_sync() whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] LOG.info("--[ retrieve changes ] %s", last_sync) # Get the item type of each synced library and build list of types to request for item_id in whitelist: library = self.server.jellyfin.get_item(item_id) library_type = library.get('CollectionType') if library_type in filters: include.append(library_type) # Include boxsets if movies are synced if 'movies' in include: include.append('boxsets') # Filter down to the list of library types we want to exclude query_filter = list(set(filters) - set(include)) try: # Get list of updates from server for synced library types and populate work queues result = self.server.jellyfin.get_sync_queue( last_sync, ",".join([x for x in query_filter])) if result is None: return True updated = [] userdata = [] removed = [] updated.extend(result['ItemsAdded']) updated.extend(result['ItemsUpdated']) userdata.extend(result['UserDataChanged']) removed.extend(result['ItemsRemoved']) total = len(updated) + len(userdata) if total > int(settings('syncIndicator') or 99): ''' Inverse yes no, in case the dialog is forced closed by Kodi. ''' if dialog("yesno", "{jellyfin}", translate(33172).replace('{number}', str(total)), nolabel=translate(107), yeslabel=translate(106)): LOG.warning("Large updates skipped.") return True self.updated(updated) self.userdata(userdata) self.removed(removed) except Exception as error: LOG.exception(error) return False return True
def backup(): ''' Jellyfin backup. ''' from helper.utils import delete_folder, copytree path = settings('backupPath') folder_name = "Kodi%s.%s" % (xbmc.getInfoLabel('System.BuildVersion')[:2], xbmc.getInfoLabel('System.Date(dd-mm-yy)')) folder_name = dialog("input", heading=translate(33089), defaultt=folder_name) if not folder_name: return backup = os.path.join(path, folder_name) if xbmcvfs.exists(backup + '/'): if not dialog("yesno", "{jellyfin}", translate(33090)): return backup() delete_folder(backup) addon_data = xbmc.translatePath( "special://profile/addon_data/plugin.video.jellyfin") destination_data = os.path.join(backup, "addon_data", "plugin.video.jellyfin") destination_databases = os.path.join(backup, "Database") if not xbmcvfs.mkdirs(path) or not xbmcvfs.mkdirs(destination_databases): LOG.info("Unable to create all directories") dialog("notification", heading="{jellyfin}", icon="{jellyfin}", message=translate(33165), sound=False) return copytree(addon_data, destination_data) databases = Objects().objects db = xbmc.translatePath(databases['jellyfin']) xbmcvfs.copy(db, os.path.join(destination_databases, db.rsplit('\\', 1)[1])) LOG.info("copied jellyfin.db") db = xbmc.translatePath(databases['video']) filename = db.rsplit('\\', 1)[1] xbmcvfs.copy(db, os.path.join(destination_databases, filename)) LOG.info("copied %s", filename) if settings('enableMusic.bool'): db = xbmc.translatePath(databases['music']) filename = db.rsplit('\\', 1)[1] xbmcvfs.copy(db, os.path.join(destination_databases, filename)) LOG.info("copied %s", filename) LOG.info("backup completed") dialog("ok", "{jellyfin}", "%s %s" % (translate(33091), backup))
def listing(): ''' Display all jellyfin nodes and dynamic entries when appropriate. ''' total = int(window('Jellyfin.nodes.total') or 0) sync = get_sync() whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] servers = get_credentials()['Servers'][1:] for i in range(total): window_prop = "Jellyfin.nodes.%s" % i path = window('%s.index' % window_prop) if not path: path = window('%s.content' % window_prop) or window( '%s.path' % window_prop) label = window('%s.title' % window_prop) node = window('%s.type' % window_prop) artwork = window('%s.artwork' % window_prop) view_id = window('%s.id' % window_prop) context = [] if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed') and view_id not in whitelist: label = "%s %s" % (label, translate(33166)) context.append(( translate(33123), "RunPlugin(plugin://plugin.video.jellyfin/?mode=synclib&id=%s)" % view_id)) if view_id and node in ('movies', 'tvshows', 'musicvideos', 'music') and view_id in whitelist: context.append((translate( 33136 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=updatelib&id=%s)" % view_id)) context.append((translate( 33132 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=repairlib&id=%s)" % view_id)) context.append((translate( 33133 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=removelib&id=%s)" % view_id)) LOG.debug("--[ listing/%s/%s ] %s", node, label, path) if path: directory(label, path, artwork=artwork, context=context) for server in servers: context = [] if server.get('ManualAddress'): context.append((translate( 33141 ), "RunPlugin(plugin://plugin.video.jellyfin/?mode=removeserver&server=%s)" % server['Id'])) if 'AccessToken' not in server: directory("%s (%s)" % (server['Name'], translate(30539)), "plugin://plugin.video.jellyfin/?mode=login&server=%s" % server['Id'], False, context=context) else: directory(server['Name'], "plugin://plugin.video.jellyfin/?mode=browse&server=%s" % server['Id'], context=context) directory(translate(33194), "plugin://plugin.video.jellyfin/?mode=managelibs", True) directory(translate(33134), "plugin://plugin.video.jellyfin/?mode=addserver", False) directory(translate(33054), "plugin://plugin.video.jellyfin/?mode=adduser", False) directory(translate(5), "plugin://plugin.video.jellyfin/?mode=settings", False) directory(translate(33161), "plugin://plugin.video.jellyfin/?mode=updatepassword", False) directory(translate(33058), "plugin://plugin.video.jellyfin/?mode=reset", False) directory(translate(33180), "plugin://plugin.video.jellyfin/?mode=restartservice", False) if settings('backupPath'): directory(translate(33092), "plugin://plugin.video.jellyfin/?mode=backup", False) xbmcplugin.setContent(PROCESS_HANDLE, 'files') xbmcplugin.endOfDirectory(PROCESS_HANDLE)
import sys from kodi_six import xbmc, xbmcaddon import database from dialogs import context from helper import translate, settings, dialog from downloader import TheVoid from helper import LazyLogger ################################################################################################# LOG = LazyLogger(__name__) XML_PATH = (xbmcaddon.Addon('plugin.video.jellyfin').getAddonInfo('path'), "default", "1080i") OPTIONS = { 'Refresh': translate(30410), 'Delete': translate(30409), 'Addon': translate(30408), 'AddFav': translate(30405), 'RemoveFav': translate(30406), 'Transcode': translate(30412) } ################################################################################################# class Context(object): _selected_option = None def __init__(self, transcode=False, delete=False):
def fast_sync(self): ''' Movie and userdata not provided by server yet. ''' last_sync = settings('LastIncrementalSync') filters = ["tvshows", "boxsets", "musicvideos", "music", "movies"] sync = get_sync() LOG.info("--[ retrieve changes ] %s", last_sync) try: updated = [] userdata = [] removed = [] for media in filters: result = self.server.jellyfin.get_sync_queue( last_sync, ",".join([x for x in filters if x != media])) updated.extend(result['ItemsAdded']) updated.extend(result['ItemsUpdated']) userdata.extend(result['UserDataChanged']) removed.extend(result['ItemsRemoved']) total = len(updated) + len(userdata) if total > int(settings('syncIndicator') or 99): ''' Inverse yes no, in case the dialog is forced closed by Kodi. ''' if dialog("yesno", heading="{jellyfin}", line1=translate(33172).replace( '{number}', str(total)), nolabel=translate(107), yeslabel=translate(106)): LOG.warning("Large updates skipped.") return True self.updated(updated) self.userdata(userdata) self.removed(removed) """ result = self.server.jellyfin.get_sync_queue(last_sync) self.userdata(result['UserDataChanged']) self.removed(result['ItemsRemoved']) filters.extend(["tvshows", "boxsets", "musicvideos", "music"]) # Get only movies. result = self.server.jellyfin.get_sync_queue(last_sync, ",".join(filters)) self.updated(result['ItemsAdded']) self.updated(result['ItemsUpdated']) self.userdata(result['UserDataChanged']) self.removed(result['ItemsRemoved']) """ except Exception as error: LOG.exception(error) return False return True
class FullSync(object): ''' This should be called like a context. i.e. with FullSync('jellyfin') as sync: sync.libraries() ''' # Borg - multiple instances, shared state _shared_state = {} sync = None running = False screensaver = None def __init__(self, library, server): ''' You can call all big syncing methods here. Initial, update, repair, remove. ''' self.__dict__ = self._shared_state if self.running: dialog("ok", heading="{jellyfin}", line1=translate(33197)) raise Exception("Sync is already running.") self.library = library self.server = server def __enter__(self): ''' Do everything we need before the sync ''' LOG.info("-->[ fullsync ]") if not settings('dbSyncScreensaver.bool'): xbmc.executebuiltin('InhibitIdleShutdown(true)') self.screensaver = get_screensaver() set_screensaver(value="") self.running = True window('jellyfin_sync.bool', True) return self def libraries(self, library_id=None, update=False): ''' Map the syncing process and start the sync. Ensure only one sync is running. ''' self.direct_path = settings('useDirectPaths') == "1" self.update_library = update self.sync = get_sync() if library_id: libraries = library_id.split(',') for selected in libraries: if selected not in [ x.replace('Mixed:', "") for x in self.sync['Libraries'] ]: library = self.get_libraries(selected) if library: self.sync['Libraries'].append("Mixed:%s" % selected if library[1] == 'mixed' else selected) if library[1] in ('mixed', 'movies'): self.sync['Libraries'].append('Boxsets:%s' % selected) else: self.sync['Libraries'].append(selected) else: self.mapping() xmls.sources() if not xmls.advanced_settings() and self.sync['Libraries']: self.start() def get_libraries(self, library_id=None): with Database('jellyfin') as jellyfindb: if library_id is None: return jellyfin_db.JellyfinDatabase( jellyfindb.cursor).get_views() else: return jellyfin_db.JellyfinDatabase( jellyfindb.cursor).get_view(library_id) def mapping(self): ''' Load the mapping of the full sync. This allows us to restore a previous sync. ''' if self.sync['Libraries']: if not dialog( "yesno", heading="{jellyfin}", line1=translate(33102)): if not dialog( "yesno", heading="{jellyfin}", line1=translate(33173)): dialog("ok", heading="{jellyfin}", line1=translate(33122)) raise LibraryException("ProgressStopped") else: self.sync['Libraries'] = [] self.sync['RestorePoint'] = {} else: LOG.info("generate full sync") libraries = [] for library in self.get_libraries(): if library[2] in ('movies', 'tvshows', 'musicvideos', 'music', 'mixed'): libraries.append({ 'Id': library[0], 'Name': library[1], 'Media': library[2] }) libraries = self.select_libraries(libraries) if [ x['Media'] for x in libraries if x['Media'] in ('movies', 'mixed') ]: self.sync['Libraries'].append("Boxsets:") save_sync(self.sync) def select_libraries(self, libraries): ''' Select all or certain libraries to be whitelisted. ''' choices = [x['Name'] for x in libraries] choices.insert(0, translate(33121)) selection = dialog("multi", translate(33120), choices) if selection is None: raise LibraryException('LibrarySelection') elif not selection: LOG.info("Nothing was selected.") raise LibraryException('SyncLibraryLater') if 0 in selection: selection = list(range(1, len(libraries) + 1)) selected_libraries = [] for x in selection: library = libraries[x - 1] if library['Media'] != 'mixed': selected_libraries.append(library['Id']) else: selected_libraries.append("Mixed:%s" % library['Id']) self.sync['Libraries'] = selected_libraries return [libraries[x - 1] for x in selection] def start(self): ''' Main sync process. ''' LOG.info("starting sync with %s", self.sync['Libraries']) save_sync(self.sync) start_time = datetime.datetime.now() for library in list(self.sync['Libraries']): self.process_library(library) if not library.startswith( 'Boxsets:') and library not in self.sync['Whitelist']: self.sync['Whitelist'].append(library) self.sync['Libraries'].pop(self.sync['Libraries'].index(library)) self.sync['RestorePoint'] = {} elapsed = datetime.datetime.now() - start_time settings('SyncInstallRunDone.bool', True) self.library.save_last_sync() save_sync(self.sync) xbmc.executebuiltin('UpdateLibrary(video)') dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33025), str(elapsed).split('.')[0]), icon="{jellyfin}", sound=False) LOG.info("Full sync completed in: %s", str(elapsed).split('.')[0]) def process_library(self, library_id): ''' Add a library by it's id. Create a node and a playlist whenever appropriate. ''' media = { 'movies': self.movies, 'musicvideos': self.musicvideos, 'tvshows': self.tvshows, 'music': self.music } try: if library_id.startswith('Boxsets:'): if library_id.endswith('Refresh'): self.refresh_boxsets() else: self.boxsets( library_id.split('Boxsets:')[1] if len(library_id) > len('Boxsets:') else None) return library = self.server.jellyfin.get_item( library_id.replace('Mixed:', "")) if library_id.startswith('Mixed:'): for mixed in ('movies', 'tvshows'): media[mixed](library) self.sync['RestorePoint'] = {} else: if library['CollectionType']: settings('enableMusic.bool', True) media[library['CollectionType']](library) except LibraryException as error: if error.status == 'StopCalled': save_sync(self.sync) raise except Exception as error: LOG.exception(error) if 'Failed to validate path' not in error: dialog("ok", heading="{jellyfin}", line1=translate(33119)) LOG.error("full sync exited unexpectedly") save_sync(self.sync) raise @contextmanager def video_database_locks(self): with self.library.database_lock: with Database() as videodb: with Database('jellyfin') as jellyfindb: yield videodb, jellyfindb @progress() def movies(self, library, dialog): ''' Process movies from a single library. ''' processed_ids = [] for items in server.get_items(library['Id'], "Movie", False, self.sync['RestorePoint'].get('params')): with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, movie in enumerate(items['Items']): dialog.update(int( (float(start_index + index) / float(items['TotalRecordCount'])) * 100), heading="%s: %s" % (translate('addon_name'), library['Name']), message=movie['Name']) obj.movie(movie, library=library) processed_ids.append(movie['Id']) with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) obj.item_ids = processed_ids if self.update_library: self.movies_compare(library, obj, jellyfindb) def movies_compare(self, library, obj, jellyfinydb): ''' Compare entries from library to what's in the jellyfindb. Remove surplus ''' db = jellyfin_db.JellyfinDatabase(jellyfinydb.cursor) items = db.get_item_by_media_folder(library['Id']) current = obj.item_ids for x in items: if x[0] not in current and x[1] == 'Movie': obj.remove(x[0]) @progress() def tvshows(self, library, dialog): ''' Process tvshows and episodes from a single library. ''' processed_ids = [] for items in server.get_items(library['Id'], "Series", False, self.sync['RestorePoint'].get('params')): with self.video_database_locks() as (videodb, jellyfindb): obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, True) self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, show in enumerate(items['Items']): percent = int((float(start_index + index) / float(items['TotalRecordCount'])) * 100) message = show['Name'] dialog.update(percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message) if obj.tvshow(show, library=library) is not False: for episodes in server.get_episode_by_show(show['Id']): for episode in episodes['Items']: dialog.update(percent, message="%s/%s" % (message, episode['Name'][:10])) obj.episode(episode) processed_ids.append(show['Id']) with self.video_database_locks() as (videodb, jellyfindb): obj = TVShows(self.server, jellyfindb, videodb, self.direct_path, True) obj.item_ids = processed_ids if self.update_library: self.tvshows_compare(library, obj, jellyfindb) def tvshows_compare(self, library, obj, jellyfindb): ''' Compare entries from library to what's in the jellyfindb. Remove surplus ''' db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) items = db.get_item_by_media_folder(library['Id']) for x in list(items): items.extend(obj.get_child(x[0])) current = obj.item_ids for x in items: if x[0] not in current and x[1] == 'Series': obj.remove(x[0]) @progress() def musicvideos(self, library, dialog): ''' Process musicvideos from a single library. ''' processed_ids = [] for items in server.get_items(library['Id'], "MusicVideo", False, self.sync['RestorePoint'].get('params')): with self.video_database_locks() as (videodb, jellyfindb): obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path) self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, mvideo in enumerate(items['Items']): dialog.update(int( (float(start_index + index) / float(items['TotalRecordCount'])) * 100), heading="%s: %s" % (translate('addon_name'), library['Name']), message=mvideo['Name']) obj.musicvideo(mvideo, library=library) processed_ids.append(mvideo['Id']) with self.video_database_locks() as (videodb, jellyfindb): obj = MusicVideos(self.server, jellyfindb, videodb, self.direct_path) obj.item_ids = processed_ids if self.update_library: self.musicvideos_compare(library, obj, jellyfindb) def musicvideos_compare(self, library, obj, jellyfindb): ''' Compare entries from library to what's in the jellyfindb. Remove surplus ''' db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) items = db.get_item_by_media_folder(library['Id']) current = obj.item_ids for x in items: if x[0] not in current and x[1] == 'MusicVideo': obj.remove(x[0]) @progress() def music(self, library, dialog): ''' Process artists, album, songs from a single library. ''' with self.library.music_database_lock: with Database('music') as musicdb: with Database('jellyfin') as jellyfindb: obj = Music(self.server, jellyfindb, musicdb, self.direct_path) for items in server.get_artists( library['Id'], False, self.sync['RestorePoint'].get('params')): self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params'][ 'StartIndex'] for index, artist in enumerate(items['Items']): percent = int( (float(start_index + index) / float(items['TotalRecordCount'])) * 100) message = artist['Name'] dialog.update( percent, heading="%s: %s" % (translate('addon_name'), library['Name']), message=message) obj.artist(artist, library=library) for albums in server.get_albums_by_artist( artist['Id']): for album in albums['Items']: obj.album(album) for songs in server.get_items( album['Id'], "Audio"): for song in songs['Items']: dialog.update( percent, message="%s/%s/%s" % (message, album['Name'][:7], song['Name'][:7])) obj.song(song) for songs in server.get_songs_by_artist( artist['Id']): for song in songs['Items']: dialog.update(percent, message="%s/%s" % (message, song['Name'])) obj.song(song) if self.update_library: self.music_compare(library, obj, jellyfindb) def music_compare(self, library, obj, jellyfindb): ''' Compare entries from library to what's in the jellyfindb. Remove surplus ''' db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) items = db.get_item_by_media_folder(library['Id']) for x in list(items): items.extend(obj.get_child(x[0])) current = obj.item_ids for x in items: if x[0] not in current and x[1] == 'MusicArtist': obj.remove(x[0]) @progress(translate(33018)) def boxsets(self, library_id=None, dialog=None): ''' Process all boxsets. ''' for items in server.get_items(library_id, "BoxSet", False, self.sync['RestorePoint'].get('params')): with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) self.sync['RestorePoint'] = items['RestorePoint'] start_index = items['RestorePoint']['params']['StartIndex'] for index, boxset in enumerate(items['Items']): dialog.update( int((float(start_index + index) / float(items['TotalRecordCount'])) * 100), heading="%s: %s" % (translate('addon_name'), translate('boxsets')), message=boxset['Name']) obj.boxset(boxset) def refresh_boxsets(self): ''' Delete all exisitng boxsets and re-add. ''' with self.video_database_locks() as (videodb, jellyfindb): obj = Movies(self.server, jellyfindb, videodb, self.direct_path) obj.boxsets_reset() self.boxsets(None) @progress(translate(33144)) def remove_library(self, library_id, dialog): ''' Remove library by their id from the Kodi database. ''' direct_path = self.library.direct_path with Database('jellyfin') as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) library = db.get_view(library_id.replace('Mixed:', "")) items = db.get_item_by_media_folder( library_id.replace('Mixed:', "")) media = 'music' if library[1] == 'music' else 'video' if media == 'music': settings('MusicRescan.bool', False) if items: count = 0 with self.library.music_database_lock if media == 'music' else self.library.database_lock: with Database(media) as kodidb: if library[1] == 'mixed': movies = [x for x in items if x[1] == 'Movie'] tvshows = [x for x in items if x[1] == 'Series'] obj = Movies(self.server, jellyfindb, kodidb, direct_path).remove for item in movies: obj(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 obj = TVShows(self.server, jellyfindb, kodidb, direct_path).remove for item in tvshows: obj(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 else: default_args = (self.server, jellyfindb, kodidb, direct_path) for item in items: if item[1] in ('Series', 'Season', 'Episode'): TVShows(*default_args).remove(item[0]) elif item[1] in ('Movie', 'BoxSet'): Movies(*default_args).remove(item[0]) elif item[1] in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): Music(*default_args).remove(item[0]) elif item[1] == 'MusicVideo': MusicVideos(*default_args).remove(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 self.sync = get_sync() if library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove(library_id) elif 'Mixed:%s' % library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove('Mixed:%s' % library_id) save_sync(self.sync) def __exit__(self, exc_type, exc_val, exc_tb): ''' Exiting sync ''' self.running = False window('jellyfin_sync', clear=True) if not settings( 'dbSyncScreensaver.bool') and self.screensaver is not None: xbmc.executebuiltin('InhibitIdleShutdown(false)') set_screensaver(value=self.screensaver) LOG.info("--<[ fullsync ]")
def remove_library(self, library_id, dialog): ''' Remove library by their id from the Kodi database. ''' direct_path = self.library.direct_path with Database('jellyfin') as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) library = db.get_view(library_id.replace('Mixed:', "")) items = db.get_item_by_media_folder( library_id.replace('Mixed:', "")) media = 'music' if library[1] == 'music' else 'video' if media == 'music': settings('MusicRescan.bool', False) if items: count = 0 with self.library.music_database_lock if media == 'music' else self.library.database_lock: with Database(media) as kodidb: if library[1] == 'mixed': movies = [x for x in items if x[1] == 'Movie'] tvshows = [x for x in items if x[1] == 'Series'] obj = Movies(self.server, jellyfindb, kodidb, direct_path).remove for item in movies: obj(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 obj = TVShows(self.server, jellyfindb, kodidb, direct_path).remove for item in tvshows: obj(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 else: default_args = (self.server, jellyfindb, kodidb, direct_path) for item in items: if item[1] in ('Series', 'Season', 'Episode'): TVShows(*default_args).remove(item[0]) elif item[1] in ('Movie', 'BoxSet'): Movies(*default_args).remove(item[0]) elif item[1] in ('MusicAlbum', 'MusicArtist', 'AlbumArtist', 'Audio'): Music(*default_args).remove(item[0]) elif item[1] == 'MusicVideo': MusicVideos(*default_args).remove(item[0]) dialog.update( int((float(count) / float(len(items)) * 100)), heading="%s: %s" % (translate('addon_name'), library[0])) count += 1 self.sync = get_sync() if library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove(library_id) elif 'Mixed:%s' % library_id in self.sync['Whitelist']: self.sync['Whitelist'].remove('Mixed:%s' % library_id) save_sync(self.sync)
def get_themes(api_client): ''' Add theme media locally, via strm. This is only for tv tunes. If another script is used, adjust this code. ''' from helper.utils import normalize_string from helper.playutils import PlayUtils from helper.xmls import tvtunes_nfo library = xbmc.translatePath( "special://profile/addon_data/plugin.video.jellyfin/library") play = settings('useDirectPaths') == "1" if not xbmcvfs.exists(library + '/'): xbmcvfs.mkdir(library) if xbmc.getCondVisibility('System.HasAddon(script.tvtunes)'): tvtunes = xbmcaddon.Addon(id="script.tvtunes") tvtunes.setSetting('custom_path_enable', "true") tvtunes.setSetting('custom_path', library) LOG.info("TV Tunes custom path is enabled and set.") else: dialog("ok", "{jellyfin}", translate(33152)) return with Database('jellyfin') as jellyfindb: all_views = jellyfin_db.JellyfinDatabase(jellyfindb.cursor).get_views() views = [ x.view_id for x in all_views if x.media_type in ('movies', 'tvshows', 'mixed') ] items = {} server = api_client.config.data['auth.server'] for view in views: result = api_client.get_items_theme_video(view) for item in result['Items']: folder = normalize_string(item['Name']) items[item['Id']] = folder result = api_client.get_items_theme_song(view) for item in result['Items']: folder = normalize_string(item['Name']) items[item['Id']] = folder for item in items: nfo_path = os.path.join(library, items[item]) nfo_file = os.path.join(nfo_path, "tvtunes.nfo") if not xbmcvfs.exists(nfo_path): xbmcvfs.mkdir(nfo_path) themes = api_client.get_themes(item) paths = [] for theme in themes['ThemeVideosResult']['Items'] + themes[ 'ThemeSongsResult']['Items']: putils = PlayUtils(theme, False, None, server, api_client) if play: paths.append(putils.direct_play(theme['MediaSources'][0])) else: paths.append(putils.direct_url(theme['MediaSources'][0])) tvtunes_nfo(nfo_file, paths) dialog("notification", heading="{jellyfin}", message=translate(33153), icon="{jellyfin}", time=1000, sound=False)
def startup(self): ''' Run at startup. Check databases. Check for the server plugin. ''' self.test_databases() Views().get_views() Views().get_nodes() try: if get_sync()['Libraries']: try: with FullSync(self, self.server) as sync: sync.libraries() Views().get_nodes() except Exception as error: LOG.exception(error) elif not settings('SyncInstallRunDone.bool'): with FullSync(self, self.server) as sync: sync.libraries() Views().get_nodes() return True if settings('SyncInstallRunDone.bool'): if settings('kodiCompanion.bool'): if self.server.jellyfin.check_companion_installed(): if not self.fast_sync(): dialog("ok", "{jellyfin}", translate(33128)) raise Exception( "Failed to retrieve latest updates") LOG.info("--<[ retrieve changes ]") else: raise LibraryException('CompanionMissing') return True except LibraryException as error: LOG.error(error.status) if error.status in 'SyncLibraryLater': dialog("ok", "{jellyfin}", translate(33129)) settings('SyncInstallRunDone.bool', True) sync = get_sync() sync['Libraries'] = [] save_sync(sync) return True elif error.status == 'CompanionMissing': dialog("ok", "{jellyfin}", translate(33099)) settings('kodiCompanion.bool', False) return True except Exception as error: LOG.exception(error) return False
def onNotification(self, sender, method, data): ''' All notifications are sent via NotifyAll built-in or Kodi. Central hub. ''' if sender.lower() not in ('plugin.video.jellyfin', 'xbmc'): return if sender == 'plugin.video.jellyfin': method = method.split('.')[1] if method not in ('ServerUnreachable', 'ServerShuttingDown', 'UserDataChanged', 'ServerConnect', 'LibraryChanged', 'ServerOnline', 'SyncLibrary', 'RepairLibrary', 'RemoveLibrary', 'SyncLibrarySelection', 'RepairLibrarySelection', 'AddServer', 'Unauthorized', 'UpdateServer', 'UserConfigurationUpdated', 'ServerRestarting', 'RemoveServer', 'AddLibrarySelection', 'RemoveLibrarySelection'): return data = json.loads(data)[0] else: if method not in ('System.OnQuit', 'System.OnSleep', 'System.OnWake'): return data = json.loads(data) LOG.debug("[ %s: %s ] %s", sender, method, json.dumps(data, indent=4)) if method == 'ServerOnline': if data.get('ServerId') is None: window('jellyfin_online.bool', True) self.settings['auth_check'] = True self.warn = True if settings('connectMsg.bool'): users = [ user for user in ( settings('additionalUsers') or "").split(',') if user ] users.insert(0, settings('username')) dialog("notification", heading="{jellyfin}", message="%s %s" % (translate(33000), ", ".join(users)), icon="{jellyfin}", time=1500, sound=False) if self.library_thread is None: self.library_thread = library.Library(self) self.library_thread.start() elif method in ('ServerUnreachable', 'ServerShuttingDown'): if self.warn or data.get('ServerId'): self.warn = data.get('ServerId') is not None dialog("notification", heading="{jellyfin}", message=translate(33146) if data.get('ServerId') is None else translate(33149), icon=xbmcgui.NOTIFICATION_ERROR) if data.get('ServerId') is None: self.stop_default() if self.waitForAbort(120): return self.start_default() elif method == 'Unauthorized': dialog("notification", heading="{jellyfin}", message=translate(33147) if data['ServerId'] is None else translate(33148), icon=xbmcgui.NOTIFICATION_ERROR) if data.get('ServerId') is None and self.settings['auth_check']: self.settings['auth_check'] = False self.stop_default() if self.waitForAbort(5): return self.start_default() elif method == 'ServerRestarting': if data.get('ServerId'): return if settings('restartMsg.bool'): dialog("notification", heading="{jellyfin}", message=translate(33006), icon="{jellyfin}") self.stop_default() if self.waitForAbort(15): return self.start_default() elif method == 'ServerConnect': self.connect.register(data['Id']) xbmc.executebuiltin("Container.Refresh") elif method == 'AddServer': self.connect.setup_manual_server() xbmc.executebuiltin("Container.Refresh") elif method == 'RemoveServer': self.connect.remove_server(data['Id']) xbmc.executebuiltin("Container.Refresh") elif method == 'UpdateServer': dialog("ok", heading="{jellyfin}", line1=translate(33151)) self.connect.setup_manual_server() elif method == 'UserDataChanged' and self.library_thread: if data.get('ServerId') or not window('jellyfin_startup.bool'): return LOG.info("[ UserDataChanged ] %s", data) self.library_thread.userdata(data['UserDataList']) elif method == 'LibraryChanged' and self.library_thread: if data.get('ServerId') or not window('jellyfin_startup.bool'): return LOG.info("[ LibraryChanged ] %s", data) self.library_thread.updated(data['ItemsUpdated'] + data['ItemsAdded']) self.library_thread.removed(data['ItemsRemoved']) elif method == 'System.OnQuit': window('jellyfin_should_stop.bool', True) self.running = False elif method in ('SyncLibrarySelection', 'RepairLibrarySelection', 'AddLibrarySelection', 'RemoveLibrarySelection'): self.library_thread.select_libraries(method) elif method == 'SyncLibrary': if not data.get('Id'): return self.library_thread.add_library(data['Id'], data.get('Update', False)) xbmc.executebuiltin("Container.Refresh") elif method == 'RepairLibrary': if not data.get('Id'): return libraries = data['Id'].split(',') for lib in libraries: if not self.library_thread.remove_library(lib): return self.library_thread.add_library(data['Id']) xbmc.executebuiltin("Container.Refresh") elif method == 'RemoveLibrary': libraries = data['Id'].split(',') for lib in libraries: if not self.library_thread.remove_library(lib): return xbmc.executebuiltin("Container.Refresh") elif method == 'System.OnSleep': LOG.info("-->[ sleep ]") window('jellyfin_should_stop.bool', True) if self.library_thread is not None: self.library_thread.stop_client() self.library_thread = None Jellyfin.close_all() self.monitor.server = [] self.monitor.sleep = True elif method == 'System.OnWake': if not self.monitor.sleep: LOG.warning( "System.OnSleep was never called, skip System.OnWake") return LOG.info("--<[ sleep ]") xbmc.sleep(10000) # Allow network to wake up self.monitor.sleep = False window('jellyfin_should_stop', clear=True) try: self.connect.register() except Exception as error: LOG.exception(error) elif method == 'GUI.OnScreensaverDeactivated': LOG.info("--<[ screensaver ]") xbmc.sleep(5000) if self.library_thread is not None: self.library_thread.fast_sync() elif method == 'UserConfigurationUpdated': if data.get('ServerId') is None: Views().get_views()
def select_libraries(self, mode=None): ''' Select from libraries synced. Either update or repair libraries. Send event back to service.py ''' modes = { 'SyncLibrarySelection': 'SyncLibrary', 'RepairLibrarySelection': 'RepairLibrary', 'AddLibrarySelection': 'SyncLibrary', 'RemoveLibrarySelection': 'RemoveLibrary' } sync = get_sync() whitelist = [x.replace('Mixed:', "") for x in sync['Whitelist']] libraries = [] with Database('jellyfin') as jellyfindb: db = jellyfin_db.JellyfinDatabase(jellyfindb.cursor) if mode in ('SyncLibrarySelection', 'RepairLibrarySelection', 'RemoveLibrarySelection'): for library in sync['Whitelist']: name = db.get_view_name(library.replace('Mixed:', "")) libraries.append({'Id': library, 'Name': name}) else: available = [ x for x in sync['SortedViews'] if x not in whitelist ] for library in available: name, media = db.get_view(library) if media in ('movies', 'tvshows', 'musicvideos', 'mixed', 'music'): libraries.append({'Id': library, 'Name': name}) choices = [x['Name'] for x in libraries] choices.insert(0, translate(33121)) titles = { "RepairLibrarySelection": 33199, "SyncLibrarySelection": 33198, "RemoveLibrarySelection": 33200, "AddLibrarySelection": 33120 } title = titles.get(mode, "Failed to get title {}".format(mode)) selection = dialog("multi", translate(title), choices) if selection is None: return if 0 in selection: selection = list(range(1, len(libraries) + 1)) selected_libraries = [] for x in selection: library = libraries[x - 1] selected_libraries.append(library['Id']) event( modes[mode], { 'Id': ','.join([libraries[x - 1]['Id'] for x in selection]), 'Update': mode == 'SyncLibrarySelection' })
def window_nodes(self): ''' Just read from the database and populate based on SortedViews Setup the window properties that reflect the jellyfin server views and more. ''' self.window_clear() self.window_clear('Jellyfin.wnodes') with Database('jellyfin') as jellyfindb: libraries = jellyfin_db.JellyfinDatabase( jellyfindb.cursor).get_views() libraries = self.order_media_folders(libraries or []) index = 0 windex = 0 try: self.media_folders = self.get_libraries() except IndexError as error: LOG.exception(error) for library in (libraries or []): view = { 'Id': library[0], 'Name': library[1], 'Tag': library[1], 'Media': library[2] } if library[0] in [ x.replace('Mixed:', "") for x in self.sync['Whitelist'] ]: # Synced libraries if view['Media'] in ('movies', 'tvshows', 'musicvideos', 'mixed'): if view['Media'] == 'mixed': for media in ('movies', 'tvshows'): for node in NODES[media]: temp_view = dict(view) temp_view['Media'] = media temp_view['Name'] = "%s (%s)" % ( view['Name'], translate(media)) self.window_node(index, temp_view, *node) self.window_wnode(windex, temp_view, *node) else: # Add one to compensate for the duplicate. index += 1 windex += 1 else: for node in NODES[view['Media']]: self.window_node(index, view, *node) if view['Media'] in ('movies', 'tvshows'): self.window_wnode(windex, view, *node) if view['Media'] in ('movies', 'tvshows'): windex += 1 elif view['Media'] == 'music': self.window_node(index, view, 'music') else: # Dynamic entry if view['Media'] in ('homevideos', 'books', 'playlists'): self.window_wnode(windex, view, 'browse') windex += 1 self.window_node(index, view, 'browse') index += 1 for single in [{ 'Name': translate('fav_movies'), 'Tag': "Favorite movies", 'Media': "movies" }, { 'Name': translate('fav_tvshows'), 'Tag': "Favorite tvshows", 'Media': "tvshows" }, { 'Name': translate('fav_episodes'), 'Tag': "Favorite episodes", 'Media': "episodes" }]: self.window_single_node(index, "favorites", single) index += 1 window('Jellyfin.nodes.total', str(index)) window('Jellyfin.wnodes.total', str(windex))
def onNotification(self, sender, method, data): if sender.lower() not in ('plugin.video.jellyfin', 'xbmc', 'upnextprovider.signal'): return if sender == 'plugin.video.jellyfin': method = method.split('.')[1] if method not in ('GetItem', 'ReportProgressRequested', 'LoadServer', 'RandomItems', 'Recommended', 'GetServerAddress', 'GetPlaybackInfo', 'Browse', 'GetImages', 'GetToken', 'PlayPlaylist', 'Play', 'GetIntros', 'GetAdditionalParts', 'RefreshItem', 'Genres', 'FavoriteItem', 'DeleteItem', 'AddUser', 'GetSession', 'GetUsers', 'GetThemes', 'GetTheme', 'Playstate', 'GeneralCommand', 'GetTranscodeOptions', 'RecentlyAdded', 'BrowseSeason', 'LiveTV', 'GetLiveStream'): return data = json.loads(data)[0] elif sender.startswith('upnextprovider'): LOG.info('Attempting to play the next episode via upnext') method = method.split('.', 1)[1] if method not in ('plugin.video.jellyfin_play_action', ): LOG.info('Received invalid upnext method: %s', method) return data = json.loads(data) method = "Play" if data: data = json.loads(binascii.unhexlify(data[0])) else: if method not in ('Player.OnPlay', 'VideoLibrary.OnUpdate', 'Player.OnAVChange'): ''' We have to clear the playlist if it was stopped before it has been played completely. Otherwise the next played item will be added the previous queue. ''' if method == "Player.OnStop": xbmc.sleep( 3000 ) # let's wait for the player so we don't clear the canceled playlist by mistake. if xbmc.getCondVisibility( "!Player.HasMedia + !Window.IsVisible(busydialog)" ): xbmc.executebuiltin("Playlist.Clear") LOG.info("[ playlist ] cleared") return data = json.loads(data) LOG.debug("[ %s: %s ] %s", sender, method, JsonDebugPrinter(data)) if self.sleep: LOG.info("System.OnSleep detected, ignore monitor request.") return try: if not data.get('ServerId'): server = Jellyfin() else: if method != 'LoadServer' and data[ 'ServerId'] not in self.servers: try: connect.Connect().register(data['ServerId']) self.server_instance(data['ServerId']) except Exception as error: LOG.exception(error) dialog("ok", heading="{jellyfin}", line1=translate(33142)) return server = Jellyfin(data['ServerId']) except Exception as error: LOG.exception(error) server = Jellyfin() if method == 'GetItem': item = server.jellyfin.get_item(data['Id']) self.void_responder(data, item) elif method == 'GetAdditionalParts': item = server.jellyfin.get_additional_parts(data['Id']) self.void_responder(data, item) elif method == 'GetIntros': item = server.jellyfin.get_intros(data['Id']) self.void_responder(data, item) elif method == 'GetImages': item = server.jellyfin.get_images(data['Id']) self.void_responder(data, item) elif method == 'GetServerAddress': server_address = server.auth.get_server_info( server.auth.server_id)['address'] self.void_responder(data, server_address) elif method == 'GetPlaybackInfo': sources = server.jellyfin.get_play_info(data['Id'], data['Profile']) self.void_responder(data, sources) elif method == 'GetLiveStream': sources = server.jellyfin.get_live_stream(data['Id'], data['PlaySessionId'], data['Token'], data['Profile']) self.void_responder(data, sources) elif method == 'GetToken': token = server.auth.jellyfin_token() self.void_responder(data, token) elif method == 'GetSession': session = server.jellyfin.get_device(self.device_id) self.void_responder(data, session) elif method == 'GetUsers': users = server.jellyfin.get_users() self.void_responder(data, users) elif method == 'GetTranscodeOptions': result = server.jellyfin.get_transcode_settings() self.void_responder(data, result) elif method == 'GetThemes': if data['Type'] == 'Video': theme = server.jellyfin.get_items_theme_video(data['Id']) else: theme = server.jellyfin.get_items_theme_song(data['Id']) self.void_responder(data, theme) elif method == 'GetTheme': theme = server.jellyfin.get_themes(data['Id']) self.void_responder(data, theme) elif method == 'Browse': result = downloader.get_filtered_section(data.get('Id'), data.get('Media'), data.get('Limit'), data.get('Recursive'), data.get('Sort'), data.get('SortOrder'), data.get('Filters'), data.get('Params'), data.get('ServerId')) self.void_responder(data, result) elif method == 'BrowseSeason': result = server.jellyfin.get_seasons(data['Id']) self.void_responder(data, result) elif method == 'LiveTV': result = server.jellyfin.get_channels() self.void_responder(data, result) elif method == 'RecentlyAdded': result = server.jellyfin.get_recently_added( data.get('Media'), data.get('Id'), data.get('Limit')) self.void_responder(data, result) elif method == 'Genres': result = server.jellyfin.get_genres(data.get('Id')) self.void_responder(data, result) elif method == 'Recommended': result = server.jellyfin.get_recommendation( data.get('Id'), data.get('Limit')) self.void_responder(data, result) elif method == 'RefreshItem': server.jellyfin.refresh_item(data['Id']) elif method == 'FavoriteItem': server.jellyfin.favorite(data['Id'], data['Favorite']) elif method == 'DeleteItem': server.jellyfin.delete_item(data['Id']) elif method == 'PlayPlaylist': server.jellyfin.post_session( server.config.data['app.session'], "Playing", { 'PlayCommand': "PlayNow", 'ItemIds': data['Id'], 'StartPositionTicks': 0 }) elif method == 'Play': items = server.jellyfin.get_items(data['ItemIds']) PlaylistWorker(data.get('ServerId'), items, data['PlayCommand'] == 'PlayNow', data.get('StartPositionTicks', 0), data.get('AudioStreamIndex'), data.get('SubtitleStreamIndex')).start() elif method in ('ReportProgressRequested', 'Player.OnAVChange'): self.player.report_playback(data.get('Report', True)) elif method == 'Playstate': self.playstate(data) elif method == 'GeneralCommand': self.general_commands(data) elif method == 'LoadServer': self.server_instance(data['ServerId']) elif method == 'AddUser': server.jellyfin.session_add_user(server.config.data['app.session'], data['Id'], data['Add']) self.additional_users(server) elif method == 'Player.OnPlay': on_play(data, server) elif method == 'VideoLibrary.OnUpdate': on_update(data, server)
def service(self): ''' If error is encountered, it will rerun this function. Start new "daemon threads" to process library updates. (actual daemon thread is not supported in Kodi) ''' self.download_threads = [ thread for thread in self.download_threads if not thread.is_done ] self.writer_threads['updated'] = [ thread for thread in self.writer_threads['updated'] if not thread.is_done ] self.writer_threads['userdata'] = [ thread for thread in self.writer_threads['userdata'] if not thread.is_done ] self.writer_threads['removed'] = [ thread for thread in self.writer_threads['removed'] if not thread.is_done ] if not self.player.isPlayingVideo() or settings( 'syncDuringPlay.bool') or xbmc.getCondVisibility( 'VideoPlayer.Content(livetv)'): self.worker_downloads() self.worker_sort() self.worker_updates() self.worker_userdata() self.worker_remove() self.worker_notify() if self.pending_refresh: window('jellyfin_sync.bool', True) if self.total_updates > self.progress_display: queue_size = self.worker_queue_size() if self.progress_updates is None: self.progress_updates = xbmcgui.DialogProgressBG() self.progress_updates.create(translate('addon_name'), translate(33178)) self.progress_updates.update( int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size)) elif queue_size: self.progress_updates.update( int((float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message="%s: %s" % (translate(33178), queue_size)) else: self.progress_updates.update(int( (float(self.total_updates - queue_size) / float(self.total_updates)) * 100), message=translate(33178)) if not settings( 'dbSyncScreensaver.bool') and self.screensaver is None: xbmc.executebuiltin('InhibitIdleShutdown(true)') self.screensaver = get_screensaver() set_screensaver(value="") if (self.pending_refresh and not self.download_threads and not self.writer_threads['updated'] and not self.writer_threads['userdata'] and not self.writer_threads['removed']): self.pending_refresh = False self.save_last_sync() self.total_updates = 0 window('jellyfin_sync', clear=True) if self.progress_updates: self.progress_updates.close() self.progress_updates = None if not settings( 'dbSyncScreensaver.bool') and self.screensaver is not None: xbmc.executebuiltin('InhibitIdleShutdown(false)') set_screensaver(value=self.screensaver) self.screensaver = None if xbmc.getCondVisibility('Container.Content(musicvideos)' ): # Prevent cursor from moving xbmc.executebuiltin('Container.Refresh') else: # Update widgets xbmc.executebuiltin('UpdateLibrary(video)') if xbmc.getCondVisibility('Window.IsMedia'): xbmc.executebuiltin('Container.Refresh')