Source code for embypy.emby

from simplejson.scanner import JSONDecodeError

import asyncio

from embypy import objects
from embypy.utils import Connector
from embypy.utils.asyncio import async_func


[docs]class Emby(objects.EmbyObject): '''Emby connection class, an object of this type should be created to communicate with emby Parameters ---------- url : str url to the server (e.g. http://127.0.0.1:8096/) api_key : str, optional key obtained from server dashboard device_id : str, optional device id to pass to emby username : str, optional username to login, this+password can be used instead of an apikey password : str, optional password for user to login as Attributes ---------- connector : embypy.utils.connector.Connector Object used to make api requests, do not use ''' def __init__(self, url, **kargs): connector = Connector(url, **kargs) super().__init__({'ItemId': '', 'Name': ''}, connector) self._partial_cache = {} self._cache_lock = asyncio.Condition() @async_func async def info(self, obj_id=None): '''Get info about object id |coro| Parameters ---------- obj_id : str, list if not provided, server info is retured(as a dict). Otherwise, an object with that id is returned (or objects if `obj_id` is a list). ''' if obj_id: try: return await self.process(obj_id) except JSONDecodeError: raise LookupError( 'Error object with that id does not exist', obj_id ) else: return await self.connector.info() @async_func async def search( self, query, sort_map={ 'BoxSet': 0, 'Series': 1, 'Movie': 2, 'Audio': 3, 'Person': 4 }, strict_sort=False ): '''Sends a search request to emby, returns results |coro| Parameters ---------- query : str the search string to send to emby sort_map : dict is a dict of strings to ints. Strings should be item types, and the ints are the priority of those types(for sorting). lower valued(0) will appear first. strict_sort : bool if True, then only item types in the keys of sortmap will be included in the results Returns ------- list list of emby objects ''' search_params = { 'remote' : False, 'searchTerm' : query } if strict_sort: search_params['IncludeItemTypes'] = ','.join(sort_map.keys()) json = await self.connector.getJson('/Search/Hints/', **search_params) items = await self.process(json["SearchHints"]) m_size = len(sort_map) return sorted(items, key=lambda x: sort_map.get(x.type, m_size)) @async_func async def latest(self, userId=None, itemTypes='', groupItems=False): '''returns list of latest items |coro| Parameters ---------- userId : str if provided, then the list returned is the one that that use will see. itemTypes: str if provided, then the list will only include items if that type - gets passed to the emby api see https://github.com/MediaBrowser/Emby/wiki/Item-Types Returns ------- list the itmes that will appear as latest (for user if id was given) ''' json = await self.connector.getJson( '/Users/{UserId}/Items/Latest', remote=False, userId=userId, IncludeItemTypes=itemTypes, GroupItems=groupItems, ) return await self.process(json) @async_func async def nextUp(self, userId=None): '''returns list of items marked as `next up` |coro| Parameters ---------- userId : str if provided, then the list returned is the one that that use will see. Returns ------- list the itmes that will appear as next up (for user if id was given) ''' json = await self.connector.getJson( '/Shows/NextUp', pass_uid=True, remote=False, userId=userId ) return await self.process(json) @async_func async def update(self): ''' reload all cached information |coro| Notes ----- This is a slow process, and will remove the cache before updating. Thus it is recomended to use the `*_force` properties, which will only update the cache after data is retrived. ''' raise RuntimeError('why was this called?') keys = self.extras.keys() self.extras = {} for key in keys: try: func = getattr(self, key, None) if asyncio.iscoroutinefunction(func): await func() elif asyncio.iscoroutine(func): await func elif callable(func): func() except asyncio.CancelledError: raise except Exception: pass @async_func async def create_playlist(self, name, *songs): '''create a new playlist |coro| Parameters ---------- name : str name of new playlist songs : array_like list of song ids to add to playlist ''' data = {'Name': name} ids = [i.id for i in (await self.process(songs))] if ids: data['Ids'] = ','.join(ids) # TODO - return playlist(s?) await self.connector.post( '/Playlists', data=data, pass_uid=True, remote=False ) async def _get_list( self, types, path='/Users/{UserId}/Items', extra_fields='', limit=200, **params ): # Note: assumes no duplicates returned by jellyfin/emby # --- # more requests = slower # bigger requests = more chances of failure # 200 items/request seems to be a nice sweetspot where I'm # not getting failures total = -1 last = -1 fields = 'Path,ParentId,Overview,PremiereDate,DateCreated' if extra_fields: fields = f'{fields},{extra_fields}' hash = (types, path, extra_fields) async with self._cache_lock: count, event, items = self._partial_cache.get(hash, (0, None, [])) if event is None: event = asyncio.Event() self._partial_cache[hash] = (count + 1, event, items) if count != 0: waiting = True while waiting: await event.wait() async with self._cache_lock: if event.is_set(): event.clear() waiting = False while len(items) != last and (len(items) < total or total == -1): try: resp = await self.connector.getJson( path, remote = False, format = 'json', recursive = 'true', includeItemTypes = types, fields = fields, sortBy = 'SortName', sortOrder = 'Ascending', startIndex = len(items), limit = limit, **params ) total = int(resp.get('TotalRecordCount', -1)) last = len(items) items.extend(resp['Items']) async with self._cache_lock: count, event, _ = self._partial_cache[hash] self._partial_cache[hash] = (count, event, items) except Exception: async with self._cache_lock: self._partial_cache[hash] = (count - 1, event, items) event.set() raise # do all the item fetching after we get the full list of item ids try: return await self.process(items) finally: async with self._cache_lock: count, event, _ = self._partial_cache[hash] event.set() if count <= 1: del self._partial_cache[hash] else: self._partial_cache[hash] = (count - 1, event, items) @property @async_func async def albums(self): '''returns list of all albums. |force| |coro| Returns ------- list of type :class:`embypy.objects.Album` ''' return self.extras.get('albums') or await self.albums_force @property @async_func async def albums_force(self): items = await self._get_list( 'MusicAlbum', extra_fields='Genres,Tags,Artists', ) self.extras['albums'] = items return items @property @async_func async def songs(self): '''returns list of all songs. |force| |coro| Returns ------- list of type :class:`embypy.objects.Audio` ''' return self.extras.get('songs') or await self.songs_force @property @async_func async def songs_force(self): items = await self._get_list( 'Audio', extra_fields='Genres,Tags,Artists', limit=300, ) self.extras['songs'] = items return items @property @async_func async def playlists(self): '''returns list of all playlists. |force| |coro| Returns ------- list of type :class:`embypy.objects.Playlist` ''' return self.extras.get('playlists') or await self.playlists_force @property @async_func async def playlists_force(self): items = await self._get_list('Playlist') self.extras['playlists'] = items return items @property @async_func async def artists(self): '''returns list of all song artists. |force| |coro| Returns ------- list of type :class:`embypy.objects.Artist` ''' return self.extras.get('artists', []) or await self.artists_force @property @async_func async def artists_force(self): items = await self._get_list( 'MusicArtist', extra_fields='Genres,Tags', ) self.extras['artists'] = items return items @property @async_func async def movies(self): '''returns list of all movies. |force| |coro| Returns ------- list of type :class:`embypy.objects.Movie` ''' return self.extras.get('movies', []) or await self.movies_force @property @async_func async def movies_force(self): items = await self._get_list( 'Movie', extra_fields='Genres,Tags,ProviderIds', limit=100, ) self.extras['movies'] = items return items @property @async_func async def series(self): '''returns a list of all series in emby. |force| |coro| Returns ------- list of type :class:`embypy.objects.Series` ''' return self.extras.get('series', []) or await self.series_force @property @async_func async def series_force(self): items = await self._get_list('Series', extra_fields='Genres,Tags') self.extras['series'] = items return items @property @async_func async def episodes(self): '''returns a list of all episodes in emby. |force| |coro| Returns ------- list of type :class:`embypy.objects.Episode` ''' return self.extras.get('episodes', []) or await self.episodes_force @property @async_func async def episodes_force(self): items = await self._get_list( 'Episode', extra_fields='Genres,Tags', limit=500, ) self.extras['episodes'] = items return items @property @async_func async def devices(self): '''returns a list of all devices connected to emby. |force| |coro| Returns ------- list of type :class:`embypy.objects.Devices` ''' return self.extras.get('devices', []) or await self.devices_force @property @async_func async def devices_force(self): items = await self.connector.getJson('/Devices', remote=False) items = await self.process(items) self.extras['devices'] = items return items @property @async_func async def users(self): '''returns a list of all users. |force| |coro| Returns ------- list of type :class:`embypy.objects.Users` ''' return self.extras.get('users', []) or await self.users_force @property @async_func async def users_force(self): items = await self.connector.getJson('/Users', remote=False) items = await self.process(items) self.extras['users'] = items return items