From 649d9cc3c6366f8fc6db4372023aa6d33035b53c Mon Sep 17 00:00:00 2001 From: Qishen Zhang Date: Sun, 23 Jan 2022 00:11:42 -0600 Subject: [PATCH] Add part of Jellyfin Library and User APIs --- Pipfile | 12 ++ Pipfile.lock | 80 +++++++++++ jellyfin_apiclient_python/api.py | 127 ++++++++++++++++++ jellyfin_apiclient_python/configuration.py | 5 +- .../connection_manager.py | 6 +- 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1105e99 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +jellyfin-apiclient-python = {editable = true, path = "."} + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..e0b39f8 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,80 @@ +{ + "_meta": { + "hash": { + "sha256": "8d248426f550cf2fb74df8e186b34f9474f80d5bfc9b8c960a0e72c4e99776aa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", + "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + ], + "markers": "python_version >= '3'", + "version": "==2.0.10" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "jellyfin-apiclient-python": { + "editable": true, + "path": "." + }, + "requests": { + "hashes": [ + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.27.1" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.8" + }, + "websocket-client": { + "hashes": [ + "sha256:1315816c0acc508997eb3ae03b9d3ff619c9d12d544c9a9b553704b1cc4f6af5", + "sha256:2eed4cc58e4d65613ed6114af2f380f7910ff416fc8c46947f6e76b6815f56c0" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + } + }, + "develop": {} +} diff --git a/jellyfin_apiclient_python/api.py b/jellyfin_apiclient_python/api.py index 2f0ba56..cca3ebb 100644 --- a/jellyfin_apiclient_python/api.py +++ b/jellyfin_apiclient_python/api.py @@ -98,6 +98,30 @@ def users(self, handler="", action="GET", params=None, json=None): return self._delete("Users/{UserId}%s" % handler, params) else: return self._get("Users/{UserId}%s" % handler, params) + + def media_folders(self, handler="", params=None, json=None): + return self._get("Library/MediaFolders/", params) + + def virtual_folders(self, handler="", action="GET", params=None, json=None): + if action == "POST": + return self._post("Library/VirtualFolders", json, params) + elif action == "DELETE": + return self._delete("Library/VirtualFolders", params) + else: + return self._get("Library/VirtualFolders", params) + + def physical_paths(self, handler="", params=None, json=None): + return self._get("Library/PhysicalPaths/", params) + + def folder_contents(self, abspath="/", params={}, json=None): + params['path'] = abspath + params['includeFiles'] = params['includeFiles'] if 'includeFiles' in params else True + params['includeDirectories'] = params['includeDirectories'] if 'includeDirectories' in params else True + return self._get("Environment/DirectoryContents", params) + + def scan_library(self): + return self._post("Library/Refresh") + def items(self, handler="", action="GET", params=None, json=None): if action == "POST": @@ -168,12 +192,95 @@ def get_public_users(self): def get_user(self, user_id=None): return self.users() if user_id is None else self._get("Users/%s" % user_id) + def create_user(self, name, password): + return self._post("Users/New", {"Name": name, "Password": password}) + + def delete_user_by_name(self, name): + deleted_users = [] + for user_info in self.get_users(): + if user_info['Name'] == name: + self._delete("Users/%s" % user_info['Id']) + deleted_users.append(user_info) + return deleted_users + def get_user_settings(self, client="emby"): return self._get("DisplayPreferences/usersettings", params={ "userId": "{UserId}", "client": client }) + # TODO: The path validation API is not working + def validate_path(self, path): + json = { + "ValidateWritable": False, + "Path": path, + "IsFile": True + } + return self._post('Environment/ValidatePath', json=json, params={}) + + def get_virtual_folders(self): + return self.virtual_folders() + + def create_virtual_folder(self, name, paths=[], collection_type='Movies', json=None, refresh_library=False): + params = { + 'name': name, + 'collectionType': collection_type, + 'paths': paths, + 'refreshLibrary': refresh_library + } + + # Just don't fetch metadata from internet by default + if json is None: + json = { + 'LibraryOptions': { + 'EnablePhotos': True, + 'EnableRealtimeMonitor': True, + 'EnableChapterImageExtraction': True, + 'ExtractChapterImagesDuringLibraryScan': True, + 'SaveLocalMetadata': False, + 'EnableInternetProviders': False, + 'EnableAutomaticSeriesGrouping': False, + 'EnableEmbeddedTitles': False, + 'EnableEmbeddedEpisodeInfos': False, + 'AutomaticRefreshIntervalDays': 0, + 'PreferredMetadataLanguage': '', + 'MetadataCountryCode': '', + 'SeasonZeroDisplayName': 'Specials', + 'MetadataSavers': [], + 'DisabledLocalMetadataReaders': [], + 'LocalMetadataReaderOrder': ['Nfo'], + 'DisabledSubtitleFetchers': [], + 'SubtitleFetcherOrder': [], + 'SkipSubtitlesIfEmbeddedSubtitlesPresent': False, + 'SkipSubtitlesIfAudioTrackMatches': False, + 'SubtitleDownloadLanguages': [], + 'RequirePerfectSubtitleMatch': True, + 'SaveSubtitlesWithMedia': True, + 'TypeOptions': [ + { + 'Type': 'Movie', + 'MetadataFetchers': ['TheMovieDb', 'The Open Movie Database'], + 'MetadataFetcherOrder': ['TheMovieDb', 'The Open Movie Database'], + 'ImageFetchers': ['Screen Grabber'], + 'ImageFetcherOrder': ['Screen Grabber'], + 'ImageOptions': [] + } + ] + } + } + return self.virtual_folders(handler="", action="POST", params=params, json=json) + + def rename_virtual_folder(self, name, new_name, refresh_library=False): + params = { + 'name': name, + 'newName': new_name, + 'refreshLibrary': refresh_library + } + return self.virtual_folders(handler="", action="POST", params=params, json={}) + + def delete_virtual_folder(self, name): + return self.virtual_folders(handler="", action="DELETE", params={'name': name}) + def get_views(self): return self.users("/Views") @@ -474,6 +581,26 @@ def send_request(self, url, path, method="get", timeout=None, headers=None, data return request_method(url, **request_settings) + # TODO: Quick connect is not responding to the requests. + def quick_connect_with_token(self, server_url, token): + path="Users/AuthenticateWithQuickConnect" + authData = {'Token': token} + + headers = self.get_default_headers() + headers.update({'Content-type': "application/json"}) + + response = self.send_request(server_url, path, method="post", headers=headers, + data=json.dumps(authData), timeout=(5, 30)) + + if response.status_code == 200: + return response.json() + else: + LOG.error("Failed to login to server with status code: " + str(response.status_code)) + LOG.error("Server Response:\n" + str(response.content)) + LOG.debug(headers) + print(headers) + return {} + def login(self, server_url, username, password=""): path = "Users/AuthenticateByName" authData = { diff --git a/jellyfin_apiclient_python/configuration.py b/jellyfin_apiclient_python/configuration.py index 1d93ef9..3e42a94 100644 --- a/jellyfin_apiclient_python/configuration.py +++ b/jellyfin_apiclient_python/configuration.py @@ -24,9 +24,12 @@ def __init__(self): LOG.debug("Configuration initializing...") self.data = {} + self.http() + self.app() + # self.auth() - def app(self, name, version, device_name, device_id, capabilities=None, device_pixel_ratio=None): + def app(self, name=None, version=None, device_name="Firefox", device_id=None, capabilities=None, device_pixel_ratio=None): LOG.debug("Begin app constructor.") self.data['app.name'] = name diff --git a/jellyfin_apiclient_python/connection_manager.py b/jellyfin_apiclient_python/connection_manager.py index 9c6a6af..efbadec 100644 --- a/jellyfin_apiclient_python/connection_manager.py +++ b/jellyfin_apiclient_python/connection_manager.py @@ -173,6 +173,10 @@ def connect_to_server(self, server, options={}): LOG.info("begin connect_to_server") + # The port in docker container is not available because it is mapped to the host port + # Replace the server address with the predefined address from options if not empty + if 'address' in options: + server.update({'address': options['address']}) try: result = self.API.get_public_info(server.get('address')) @@ -357,7 +361,7 @@ def _after_connect_validated(self, server, credentials, system_info, verify_auth self.config.data['auth.server'] = server['address'] self.config.data['auth.server-name'] = server['Name'] self.config.data['auth.server=id'] = server['Id'] - self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl']) + self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl'] if 'auth.ssl' in self.config.data else None) result = { 'Servers': [server]