From 4a8391853cd1b49bf37868b0de453be7de83d603 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Fri, 15 Nov 2024 20:06:05 +1000 Subject: [PATCH] Fix #53: Handle trailing slashes in jellyfin_url --- jellyfin_apiclient_python/api.py | 91 ++++++++++++++++++++++++++++++-- tests/test_api.py | 13 +++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tests/test_api.py diff --git a/jellyfin_apiclient_python/api.py b/jellyfin_apiclient_python/api.py index 37e3fdb..64f717a 100644 --- a/jellyfin_apiclient_python/api.py +++ b/jellyfin_apiclient_python/api.py @@ -13,7 +13,8 @@ def jellyfin_url(client, handler): - return "%s/%s" % (client.config.data['auth.server'], handler) + base_url = client.config.data['auth.server'].rstrip('/') + return f"{base_url}/{handler.lstrip('/')}" def basic_info(): @@ -146,6 +147,31 @@ def refresh_library(self): """ return self._post("Library/Refresh") + def add_media_library(self, name, collectionType, paths, refreshLibrary=True): + """ + Create a new media library. + + Args: + name (str): name of the new library + + collectionType (str): one of "movies" "tvshows" "music" "musicvideos" + "homevideos" "boxsets" "books" "mixed" + + paths (List[str]): + paths on the server to use in the media library + + References: + .. [AddVirtualFolder] https://api.jellyfin.org/#tag/LibraryStructure/operation/AddVirtualFolder + """ + params = { + 'name': name, + 'collectionType': collectionType, + 'paths': paths, + 'refreshLibrary': refreshLibrary, + + } + return self.virtual_folders('POST', params=params) + def items(self, handler="", action="GET", params=None, json=None): if action == "POST": return self._post("Items%s" % handler, json, params) @@ -547,6 +573,34 @@ def favorite(self, item_id, option=True): def get_system_info(self): return self._get("System/Configuration") + def get_server_logs(self): + """ + Returns: + List[Dict] - list of information about available log files + + References: + .. [GetServerLogs] https://api.jellyfin.org/#tag/System/operation/GetServerLogs + """ + return self._get("System/Logs") + + def get_log_entries(self, startIndex=None, limit=None, minDate=None, hasUserId=None): + """ + Returns a list of recent log entries + + Returns: + Dict: with main key "Items" + """ + params = {} + if limit is not None: + params['limit'] = limit + if startIndex is not None: + params['startIndex'] = startIndex + if minDate is not None: + params['minDate'] = minDate + if hasUserId is not None: + params['hasUserId'] = hasUserId + return self._get("System/ActivityLog/Entries", params=params) + def post_capabilities(self, data): return self.sessions("/Capabilities/Full", "POST", json=data) @@ -634,19 +688,24 @@ def get_sync_queue(self, date, filters=None): def get_server_time(self): return self._get("Jellyfin.Plugin.KodiSyncQueue/GetServerDateTime") - def get_play_info(self, item_id, profile, aid=None, sid=None, start_time_ticks=None, is_playback=True): + def get_play_info(self, item_id, profile=None, aid=None, sid=None, start_time_ticks=None, is_playback=True): args = { 'UserId': "{UserId}", - 'DeviceProfile': profile, 'AutoOpenLiveStream': is_playback, 'IsPlayback': is_playback } + if profile is None: + args['DeviceProfile'] = profile if sid: args['SubtitleStreamIndex'] = sid if aid: args['AudioStreamIndex'] = aid if start_time_ticks: args['StartTimeTicks'] = start_time_ticks + # TODO: + # Should this be a get? + # https://api.jellyfin.org/#tag/MediaInfo + # https://api.jellyfin.org/#tag/MediaInfo/operation/GetPostedPlaybackInfo return self.items("/%s/PlaybackInfo" % item_id, "POST", json=args) def get_live_stream(self, item_id, play_id, token, profile): @@ -899,6 +958,32 @@ def identify(client, item_id, provider_ids): body = {'ProviderIds': provider_ids} return client.jellyfin.items('/RemoteSearch/Apply/' + item_id, action='POST', params=None, json=body) + def get_now_playing(self, session_id): + """ + Simplified API to get now playing information for a session including the + play state. + + References: + https://github.com/jellyfin/jellyfin/issues/9665 + """ + resp = self.sessions(params={ + 'Id': session_id, + 'fields': ['PlayState'] + }) + found = None + for item in resp: + if item['Id'] == session_id: + found = item + if not found: + raise KeyError(f'No session_id={session_id}') + play_state = found['PlayState'] + now_playing = found.get('NowPlayingItem', None) + if now_playing is None: + # handle case if nothing is playing + now_playing = {'Name': None} + now_playing['PlayState'] = play_state + return now_playing + class CollectionAPIMixin: """ diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..a741b3b --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,13 @@ +from jellyfin_apiclient_python.api import jellyfin_url +from unittest.mock import Mock + +def test_jellyfin_url_handles_trailing_slash(): + mock_client = Mock() + mock_client.config.data = {'auth.server': 'https://example.com/'} + handler = "Items/1234" + url = jellyfin_url(mock_client, handler) + assert url == "https://example.com/Items/1234" + + mock_client.config.data = {'auth.server': 'https://example.com'} + url = jellyfin_url(mock_client, handler) + assert url == "https://example.com/Items/1234"