Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add part of Jellyfin Library and User APIs #11

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -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"
iwalton3 marked this conversation as resolved.
Show resolved Hide resolved
80 changes: 80 additions & 0 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions jellyfin_apiclient_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +118 to +119
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
params['includeFiles'] = params['includeFiles'] if 'includeFiles' in params else True
params['includeDirectories'] = params['includeDirectories'] if 'includeDirectories' in params else True
params['includeFiles'] = params.get('includeFiles', True)
params['includeDirectories'] = params.get('includeDirectories', 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":
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the Jellyfin API call doesn't work or there is a problem with this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API call doesn't work and I guess it's the problem on the server side.

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()
iwalton3 marked this conversation as resolved.
Show resolved Hide resolved

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")

Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, any idea why this may be an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry the Jellyfin is like a blackbox to me and I only make some calls to see if the server can return me some information. The quick connect is not working at all. By the way, my initial thought is to use an authorization token to create a session rather than use username, password or PIN to log in. If you know it, please send me a link to the API call. Thanks!

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 = {
Expand Down
5 changes: 4 additions & 1 deletion jellyfin_apiclient_python/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name=None, version=None, device_name="Firefox", device_id=None

I do know that some functions of the Jellyfin API refuse to work without a correct device name or id. It has also been known to have websocket connectivity break when the client name is changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's why I have to call app() to make it work. We should put some comments on the function to mention that you need to switch app() on and off if some API calls are not working due to the headers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name=None, version=None, device_name="Firefox", device_id=None

I do know that some functions of the Jellyfin API refuse to work without a correct device name or id. It has also been known to have websocket connectivity break when the client name is changed.


LOG.debug("Begin app constructor.")
self.data['app.name'] = name
Expand Down
6 changes: 5 additions & 1 deletion jellyfin_apiclient_python/connection_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.config.data['auth.ssl'] = options.get('ssl', self.config.data['auth.ssl'] if 'auth.ssl' in self.config.data else None)
self.config.data['auth.ssl'] = options.get('ssl', self.config.data.get('auth.ssl'))


result = {
'Servers': [server]
Expand Down