# -*- coding: utf-8 -*- ################################################################################################# import logging import hashlib import threading import Queue import xbmc import downloadutils import database from utils import window, settings from contextlib import closing ################################################################################################# log = logging.getLogger("EMBY."+__name__) ################################################################################################# class DownloadThreader(threading.Thread): is_finished = False def __init__(self, queue, output): self.queue = queue self.output = output threading.Thread.__init__(self) def run(self): try: query = self.queue.get() except Queue.Empty: self.is_finished = True return try: result = downloadutils.DownloadUtils().downloadUrl(query['url'], parameters=query.get('params')) if result: self.output.extend(result['Items']) except Exception as error: log.error(error) self.queue.task_done() self.is_finished = True class Read_EmbyServer(): limitIndex = min(int(settings('limitIndex')), 50) download_limit = int(settings('downloadThreads')) download_threads = list() def __init__(self): self.doUtils = downloadutils.DownloadUtils() self.userId = window('emby_currUser') self.server = window('emby_server%s' % self.userId) def get_emby_url(self, handler): return "{server}/emby/%s" % handler def _add_worker_thread(self, queue, output): while True: for thread in self.download_threads: if thread.is_finished: self.download_threads.remove(thread) if window('emby_online') != "true": # Something happened log.error("Server is not online, don't start new download thread") queue.task_done() return False if len(self.download_threads) < self.download_limit: # Start new "daemon thread" - actual daemon thread is not supported in Kodi new_thread = DownloadThreader(queue, output) counter = 0 worked = False while counter < 10: try: new_thread.start() worked = True break except: counter = counter + 1 xbmc.sleep(1000) if worked: self.download_threads.append(new_thread) return True else: return False else: log.info("Waiting for empty download spot: %s", len(self.download_threads)) xbmc.sleep(100) def split_list(self, itemlist, size): # Split up list in pieces of size. Will generate a list of lists return [itemlist[i:i+size] for i in range(0, len(itemlist), size)] def getItem(self, itemid): # This will return the full item item = self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items/%s?format=json" % itemid) return item def getItems(self, item_list): items = [] queue = Queue.Queue() url = "{server}/emby/Users/{UserId}/Items?&format=json" for item_ids in self.split_list(item_list, self.limitIndex): # Will return basic information params = { 'Ids': ",".join(item_ids), 'Fields': "Etag" } queue.put({'url': url, 'params': params}) if not self._add_worker_thread(queue, items): break queue.join() return items def getFullItems(self, item_list): items = [] queue = Queue.Queue() url = "{server}/emby/Users/{UserId}/Items?format=json" for item_ids in self.split_list(item_list, self.limitIndex): params = { "Ids": ",".join(item_ids), "Fields": ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "MediaSources,VoteCount" ) } queue.put({'url': url, 'params': params}) if not self._add_worker_thread(queue, items): break queue.join() return items def getFilteredSection(self, parentid, itemtype=None, sortby="SortName", recursive=True, limit=None, sortorder="Ascending", filter_type=""): params = { 'ParentId': parentid, 'IncludeItemTypes': itemtype, 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, 'IsMissing': False, 'Recursive': recursive, 'Limit': limit, 'SortBy': sortby, 'SortOrder': sortorder, 'Filters': filter_type, 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" ) } return self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) def getTvChannels(self): params = { 'EnableImages': True, 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" ) } url = "{server}/emby/LiveTv/Channels/?userid={UserId}&format=json" return self.doUtils.downloadUrl(url, parameters=params) def getTvRecordings(self, groupid): if groupid == "root": groupid = "" params = { 'GroupId': groupid, 'EnableImages': True, 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers" ) } url = "{server}/emby/LiveTv/Recordings/?userid={UserId}&format=json" return self.doUtils.downloadUrl(url, parameters=params) def getSection(self, parentid, itemtype=None, sortby="SortName", artist_id=None, basic=False, dialog=None): items = { 'Items': [], 'TotalRecordCount': 0 } # Get total number of items url = "{server}/emby/Users/{UserId}/Items?format=json" params = { 'ParentId': parentid, 'ArtistIds': artist_id, 'IncludeItemTypes': itemtype, 'LocationTypes': "FileSystem,Remote,Offline", 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, 'IsMissing': False, 'Recursive': True, 'Limit': 1 } try: result = self.doUtils.downloadUrl(url, parameters=params) total = result['TotalRecordCount'] items['TotalRecordCount'] = total except Exception as error: # Failed to retrieve log.debug("%s:%s Failed to retrieve the server response: %s", url, params, error) else: index = 0 jump = self.limitIndex queue = Queue.Queue() while index < total: # Get items by chunk to increase retrieval speed at scale params = { 'ParentId': parentid, 'ArtistIds': artist_id, 'IncludeItemTypes': itemtype, 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'IsMissing': False, 'Recursive': True, 'StartIndex': index, 'Limit': jump, 'SortBy': sortby, 'SortOrder': "Ascending", } if basic: params['Fields'] = "Etag" else: params['Fields'] = ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "MediaSources,VoteCount" ) queue.put({'url': url, 'params': params}) if not self._add_worker_thread(queue, items['Items']): break index += jump if dialog: percentage = int((float(index) / float(total))*100) dialog.update(percentage) queue.join() if dialog: dialog.update(100) return items def get_views(self, root=False): if not root: url = "{server}/emby/Users/{UserId}/Views?format=json" else: # Views ungrouped url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" return self.doUtils.downloadUrl(url) def getViews(self, mediatype="", root=False, sortedlist=False): # Build a list of user views views = [] mediatype = mediatype.lower() try: items = self.get_views(root)['Items'] except Exception as error: log.debug("Error retrieving views for type: %s error:%s" % (mediatype, error)) else: for item in items: if item['Type'] in ("Channel", "PlaylistsFolder"): # Filter view types continue # 3/4/2016 OriginalCollectionType is added itemtype = item.get('OriginalCollectionType', item.get('CollectionType', "mixed")) if item['Name'] not in ('Collections', 'Trailers', 'Playlists'): if sortedlist: views.append({ 'name': item['Name'], 'type': itemtype, 'id': item['Id'] }) elif (itemtype == mediatype or (itemtype == "mixed" and mediatype in ("movies", "tvshows"))): views.append({ 'name': item['Name'], 'type': itemtype, 'id': item['Id'] }) return views def verifyView(self, parentid, itemid): params = { 'ParentId': parentid, 'CollapseBoxSetItems': False, 'IsVirtualUnaired': False, 'LocationTypes': "FileSystem,Remote,Offline", 'IsMissing': False, 'Recursive': True, 'Ids': itemid } try: result = self.doUtils.downloadUrl("{server}/emby/Users/{UserId}/Items?format=json", parameters=params) total = result['TotalRecordCount'] except Exception as error: # Something happened to the connection log.info("Error getting item count: " + str(error)) return False return True if total else False def getMovies(self, parentId, basic=False, dialog=None): return self.getSection(parentId, "Movie", basic=basic, dialog=dialog) def getBoxset(self, dialog=None): return self.getSection(None, "BoxSet", dialog=dialog) def getMovies_byBoxset(self, boxsetid): return self.getSection(boxsetid, "Movie") def getMusicVideos(self, parentId, basic=False, dialog=None): return self.getSection(parentId, "MusicVideo", basic=basic, dialog=dialog) def getHomeVideos(self, parentId): return self.getSection(parentId, "Video") def getShows(self, parentId, basic=False, dialog=None): return self.getSection(parentId, "Series", basic=basic, dialog=dialog) def getSeasons(self, showId): items = { 'Items': [], 'TotalRecordCount': 0 } params = { 'IsVirtualUnaired': False, 'Fields': "Etag" } url = "{server}/emby/Shows/%s/Seasons?UserId={UserId}&format=json" % showId try: result = self.doUtils.downloadUrl(url, parameters=params) except Exception as error: log.info("Error getting Seasons form server: " + str(error)) result = None if result is not None: items = result return items def getEpisodes(self, parentId, basic=False, dialog=None): return self.getSection(parentId, "Episode", basic=basic, dialog=dialog) def getEpisodesbyShow(self, showId): return self.getSection(showId, "Episode") def getEpisodesbySeason(self, seasonId): return self.getSection(seasonId, "Episode") def getArtists(self, parent_id=None, dialog=None): items = { 'Items': [], 'TotalRecordCount': 0 } # Get total number of items url = "{server}/emby/Artists?UserId={UserId}&format=json" params = { 'ParentId': parent_id, 'Recursive': True, 'Limit': 1 } try: result = self.doUtils.downloadUrl(url, parameters=params) total = result['TotalRecordCount'] items['TotalRecordCount'] = total except Exception as error: # Failed to retrieve log.debug("%s:%s Failed to retrieve the server response: %s", url, params, error) else: index = 0 jump = self.limitIndex queue = Queue.Queue() while index < total: # Get items by chunk to increase retrieval speed at scale params = { 'ParentId': parent_id, 'Recursive': True, 'IsVirtualUnaired': False, 'EnableTotalRecordCount': False, 'LocationTypes': "FileSystem,Remote,Offline", 'IsMissing': False, 'StartIndex': index, 'Limit': jump, 'SortBy': "SortName", 'SortOrder': "Ascending", 'Fields': ( "Etag,Genres,SortName,Studios,Writer,ProductionYear," "CommunityRating,OfficialRating,CumulativeRunTimeTicks,Metascore," "AirTime,DateCreated,MediaStreams,People,ProviderIds,Overview" ) } queue.put({'url': url, 'params': params}) if not self._add_worker_thread(queue, items['Items']): break index += jump if dialog: percentage = int((float(index) / float(total))*100) dialog.update(percentage) queue.join() if dialog: dialog.update(100) return items def getAlbums(self, basic=False, dialog=None): return self.getSection(None, "MusicAlbum", sortby="DateCreated", basic=basic, dialog=dialog) def getAlbumsbyArtist(self, artistId): return self.getSection(None, "MusicAlbum", sortby="DateCreated", artist_id=artistId) def getSongs(self, basic=False, dialog=None): return self.getSection(None, "Audio", basic=basic, dialog=dialog) def getSongsbyAlbum(self, albumId): return self.getSection(albumId, "Audio") def getAdditionalParts(self, itemId): items = { 'Items': [], 'TotalRecordCount': 0 } url = "{server}/emby/Videos/%s/AdditionalParts?UserId={UserId}&format=json" % itemId try: result = self.doUtils.downloadUrl(url) except Exception as error: log.info("Error getting additional parts form server: " + str(error)) result = None if result is not None: items = result return items def sortby_mediatype(self, itemids): sorted_items = {} # Sort items items = self.getFullItems(itemids) for item in items: mediatype = item.get('Type') if mediatype: sorted_items.setdefault(mediatype, []).append(item) return sorted_items def updateUserRating(self, itemid, favourite=None): # Updates the user rating to Emby doUtils = self.doUtils.downloadUrl if favourite: url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid doUtils(url, action_type="POST") elif not favourite: url = "{server}/emby/Users/{UserId}/FavoriteItems/%s?format=json" % itemid doUtils(url, action_type="DELETE") else: log.info("Error processing user rating.") log.info("Update user rating to emby for itemid: %s | favourite: %s" % (itemid, favourite)) def refreshItem(self, itemid): url = "{server}/emby/Items/%s/Refresh?format=json" % itemid params = { 'Recursive': True, 'ImageRefreshMode': "FullRefresh", 'MetadataRefreshMode': "FullRefresh", 'ReplaceAllImages': False, 'ReplaceAllMetadata': True } self.doUtils.downloadUrl(url, postBody=params, action_type="POST") def deleteItem(self, itemid): url = "{server}/emby/Items/%s?format=json" % itemid self.doUtils.downloadUrl(url, action_type="DELETE") def getUsers(self, server): url = "%s/emby/Users/Public?format=json" % server try: users = self.doUtils.downloadUrl(url, authenticate=False) except Exception as error: log.info("Error getting users from server: " + str(error)) users = [] return users def loginUser(self, server, username, password=None): password = password or "" url = "%s/emby/Users/AuthenticateByName?format=json" % server data = {'username': username, 'password': hashlib.sha1(password).hexdigest()} user = self.doUtils.downloadUrl(url, postBody=data, action_type="POST", authenticate=False) return user def get_single_item(self, media_type, parent_id): params = { 'ParentId': parent_id, 'Recursive': True, 'Limit': 1, 'IncludeItemTypes': media_type } url = self.get_emby_url('Users/{UserId}/Items?format=json') return self.doUtils.downloadUrl(url, parameters=params) # NEW CODE ---------------------------------------------------- @classmethod def _get_full_details(cls, params): params.update({ 'Fields': ( "Path,Genres,SortName,Studios,Writer,ProductionYear,Taglines," "CommunityRating,OfficialRating,CumulativeRunTimeTicks," "Metascore,AirTime,DateCreated,MediaStreams,People,Overview," "CriticRating,CriticRatingSummary,Etag,ShortOverview,ProductionLocations," "Tags,ProviderIds,ParentId,RemoteTrailers,SpecialEpisodeNumbers," "MediaSources,VoteCount" ) }) return params def get_parent_child(self, parent_id, media_format=None): url = self.get_emby_url('Users/{UserId}/Items') params = { 'SortBy': "SortName", 'SortOrder': "Ascending", 'IncludeItemTypes': media_format, 'Recursive': True, 'Limit': 1, 'ParentId': parent_id } result = self.doUtils.downloadUrl(url, parameters=params) params['Limit'] = self.limitIndex params = self._get_full_details(params) index = 0 while index < result['TotalRecordCount']: params['StartIndex'] = index yield self.doUtils.downloadUrl(url, parameters=params) index += self.limitIndex def get_view_options(self, view_id): url = self.get_emby_url('Library/VirtualFolders') for library in self.doUtils.downloadUrl(url): if library['ItemId'] == view_id: return library['LibraryOptions'] def get_server_transcoding_settings(self): url = self.get_emby_url('/System/Configuration/encoding') return self.doUtils.downloadUrl(url)