2020-04-11 14:20:25 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import requests
|
2020-05-09 21:10:30 +00:00
|
|
|
import time
|
2020-04-11 14:20:25 +00:00
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class Error(Exception):
|
|
|
|
pass
|
|
|
|
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class Jellyfin:
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Basic Jellyfin API client, providing account related function only.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class UserExistsError(Error):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Thrown if a user already exists with the same name
|
|
|
|
when creating an account.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
pass
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class UserNotFoundError(Error):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""Thrown if account with specified user ID/name does not exist."""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
pass
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class AuthenticationError(Error):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""Thrown if authentication with Jellyfin fails."""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
pass
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
class AuthenticationRequiredError(Error):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Thrown if privileged action is attempted without authentication.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
pass
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-06 15:01:38 +00:00
|
|
|
class UnknownError(Error):
|
|
|
|
"""
|
|
|
|
Thrown if i've been too lazy to figure out an error's meaning.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-06 15:01:38 +00:00
|
|
|
pass
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-04-11 14:20:25 +00:00
|
|
|
def __init__(self, server, client, version, device, deviceId):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Initializes the Jellyfin object. All parameters except server
|
|
|
|
have no effect on the client's capability.
|
|
|
|
|
|
|
|
:param server: Web address of the server to connect to.
|
|
|
|
:param client: Name of the client. Appears on Jellyfin
|
|
|
|
server dashboard.
|
|
|
|
:param version: Version of the client.
|
|
|
|
:param device: Name of the device the client is running on.
|
|
|
|
:param deviceId: ID of the device the client is running on.
|
|
|
|
"""
|
2020-04-11 14:20:25 +00:00
|
|
|
self.server = server
|
|
|
|
self.client = client
|
|
|
|
self.version = version
|
|
|
|
self.device = device
|
|
|
|
self.deviceId = deviceId
|
2020-05-09 21:10:30 +00:00
|
|
|
self.timeout = 30 * 60
|
|
|
|
self.userCacheAge = time.time() - self.timeout - 1
|
|
|
|
self.userCachePublicAge = self.userCacheAge
|
2020-04-11 14:20:25 +00:00
|
|
|
self.useragent = f"{self.client}/{self.version}"
|
|
|
|
self.auth = "MediaBrowser "
|
|
|
|
self.auth += f"Client={self.client}, "
|
|
|
|
self.auth += f"Device={self.device}, "
|
|
|
|
self.auth += f"DeviceId={self.deviceId}, "
|
|
|
|
self.auth += f"Version={self.version}"
|
|
|
|
self.header = {
|
2020-06-21 19:29:53 +00:00
|
|
|
"Accept": "application/json",
|
|
|
|
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
|
|
"X-Application": f"{self.client}/{self.version}",
|
|
|
|
"Accept-Charset": "UTF-8,*",
|
|
|
|
"Accept-encoding": "gzip",
|
|
|
|
"User-Agent": self.useragent,
|
|
|
|
"X-Emby-Authorization": self.auth,
|
2020-04-11 14:20:25 +00:00
|
|
|
}
|
2020-06-27 14:38:51 +00:00
|
|
|
self.info = requests.get(self.server + "/System/Info/Public").json()
|
2020-06-21 19:29:53 +00:00
|
|
|
|
|
|
|
def getUsers(self, username: str = "all", userId: str = "all", public: bool = True):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Returns details on user(s), such as ID, Name, Policy.
|
|
|
|
|
|
|
|
:param username: (optional) Username to get info about.
|
|
|
|
Leave blank to get all users.
|
2020-06-21 19:21:33 +00:00
|
|
|
:param userId: (optional) User ID to get info about.
|
2020-05-24 14:19:39 +00:00
|
|
|
Leave blank to get all users.
|
|
|
|
:param public: True = Get publicly visible users only (no auth required),
|
|
|
|
False = Get all users (auth required).
|
|
|
|
"""
|
2020-04-26 18:44:31 +00:00
|
|
|
if public is True:
|
2020-05-09 21:10:30 +00:00
|
|
|
if (time.time() - self.userCachePublicAge) >= self.timeout:
|
2020-06-21 19:29:53 +00:00
|
|
|
response = requests.get(self.server + "/emby/Users/Public").json()
|
2020-05-09 21:10:30 +00:00
|
|
|
self.userCachePublic = response
|
|
|
|
self.userCachePublicAge = time.time()
|
|
|
|
else:
|
|
|
|
response = self.userCachePublic
|
2020-06-21 19:29:53 +00:00
|
|
|
elif (
|
|
|
|
public is False and hasattr(self, "username") and hasattr(self, "password")
|
|
|
|
):
|
2020-05-09 21:10:30 +00:00
|
|
|
if (time.time() - self.userCacheAge) >= self.timeout:
|
2020-06-21 19:29:53 +00:00
|
|
|
response = requests.get(
|
|
|
|
self.server + "/emby/Users",
|
|
|
|
headers=self.header,
|
|
|
|
params={"Username": self.username, "Pw": self.password},
|
|
|
|
)
|
2020-05-09 21:10:30 +00:00
|
|
|
if response.status_code == 200:
|
|
|
|
response = response.json()
|
|
|
|
self.userCache = response
|
|
|
|
self.userCacheAge = time.time()
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self.authenticate(self.username, self.password)
|
2020-06-27 14:38:51 +00:00
|
|
|
return self.getUsers(username, userId, public)
|
2020-05-09 21:10:30 +00:00
|
|
|
except self.AuthenticationError:
|
|
|
|
raise self.AuthenticationRequiredError
|
2020-04-20 19:37:39 +00:00
|
|
|
else:
|
2020-05-09 21:10:30 +00:00
|
|
|
response = self.userCache
|
2020-04-26 18:44:31 +00:00
|
|
|
else:
|
|
|
|
raise self.AuthenticationRequiredError
|
2020-06-27 14:38:51 +00:00
|
|
|
if username == "all" and userId == "all":
|
2020-04-20 19:37:39 +00:00
|
|
|
return response
|
2020-06-27 14:38:51 +00:00
|
|
|
elif userId == "all":
|
2020-04-11 14:20:25 +00:00
|
|
|
match = False
|
|
|
|
for user in response:
|
2020-06-21 19:29:53 +00:00
|
|
|
if user["Name"] == username:
|
2020-04-11 14:20:25 +00:00
|
|
|
match = True
|
|
|
|
return user
|
|
|
|
if not match:
|
|
|
|
raise self.UserNotFoundError
|
2020-04-20 19:37:39 +00:00
|
|
|
else:
|
|
|
|
match = False
|
|
|
|
for user in response:
|
2020-06-27 14:38:51 +00:00
|
|
|
if user["Id"] == userId:
|
2020-04-20 19:37:39 +00:00
|
|
|
match = True
|
|
|
|
return user
|
|
|
|
if not match:
|
|
|
|
raise self.UserNotFoundError
|
2020-06-21 19:21:33 +00:00
|
|
|
|
|
|
|
def authenticate(self, username: str, password: str):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Authenticates by name with Jellyfin.
|
|
|
|
|
|
|
|
:param username: Plaintext username.
|
|
|
|
:param password: Plaintext password.
|
|
|
|
"""
|
2020-04-11 14:20:25 +00:00
|
|
|
self.username = username
|
|
|
|
self.password = password
|
2020-06-21 19:29:53 +00:00
|
|
|
response = requests.post(
|
|
|
|
self.server + "/emby/Users/AuthenticateByName",
|
|
|
|
headers=self.header,
|
|
|
|
params={"Username": self.username, "Pw": self.password},
|
|
|
|
)
|
2020-04-11 14:20:25 +00:00
|
|
|
if response.status_code == 200:
|
|
|
|
json = response.json()
|
2020-06-21 19:29:53 +00:00
|
|
|
self.userId = json["User"]["Id"]
|
|
|
|
self.accessToken = json["AccessToken"]
|
2020-04-22 20:54:31 +00:00
|
|
|
self.auth = "MediaBrowser "
|
|
|
|
self.auth += f"Client={self.client}, "
|
|
|
|
self.auth += f"Device={self.device}, "
|
|
|
|
self.auth += f"DeviceId={self.deviceId}, "
|
|
|
|
self.auth += f"Version={self.version}"
|
2020-04-11 14:20:25 +00:00
|
|
|
self.auth += f", Token={self.accessToken}"
|
2020-06-21 19:29:53 +00:00
|
|
|
self.header["X-Emby-Authorization"] = self.auth
|
2020-06-27 14:38:51 +00:00
|
|
|
self.info = requests.get(
|
|
|
|
self.server + "/System/Info", headers=self.header
|
|
|
|
).json()
|
2020-04-11 14:20:25 +00:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
raise self.AuthenticationError
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def setPolicy(self, userId: str, policy: dict):
|
2020-05-24 14:19:39 +00:00
|
|
|
"""
|
|
|
|
Sets a user's policy (Admin rights, Library Access, etc.) by user ID.
|
|
|
|
|
|
|
|
:param userId: ID of the user to modify.
|
|
|
|
:param policy: User policy in dictionary form.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
return requests.post(
|
|
|
|
self.server + "/Users/" + userId + "/Policy",
|
|
|
|
headers=self.header,
|
|
|
|
params=policy,
|
|
|
|
)
|
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def newUser(self, username: str, password: str):
|
2020-04-11 14:20:25 +00:00
|
|
|
for user in self.getUsers():
|
2020-06-21 19:29:53 +00:00
|
|
|
if user["Name"] == username:
|
2020-04-11 14:20:25 +00:00
|
|
|
raise self.UserExistsError
|
2020-06-21 19:29:53 +00:00
|
|
|
response = requests.post(
|
|
|
|
self.server + "/emby/Users/New",
|
|
|
|
headers=self.header,
|
|
|
|
params={"Name": username, "Password": password},
|
|
|
|
)
|
2020-04-11 14:20:25 +00:00
|
|
|
if response.status_code == 401:
|
2020-06-21 19:29:53 +00:00
|
|
|
if hasattr(self, "username") and hasattr(self, "password"):
|
2020-04-26 18:44:31 +00:00
|
|
|
self.authenticate(self.username, self.password)
|
|
|
|
return self.newUser(username, password)
|
|
|
|
else:
|
|
|
|
raise self.AuthenticationRequiredError
|
2020-04-11 14:20:25 +00:00
|
|
|
return response
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def getViewOrder(self, userId: str, public: bool = True):
|
2020-06-06 15:01:38 +00:00
|
|
|
if not public:
|
2020-06-21 19:29:53 +00:00
|
|
|
param = "?IncludeHidden=true"
|
2020-06-06 15:01:38 +00:00
|
|
|
else:
|
2020-06-21 19:29:53 +00:00
|
|
|
param = ""
|
|
|
|
views = requests.get(
|
|
|
|
self.server + "/Users/" + userId + "/Views" + param, headers=self.header
|
|
|
|
).json()["Items"]
|
2020-06-06 15:01:38 +00:00
|
|
|
orderedViews = []
|
|
|
|
for library in views:
|
2020-06-21 19:29:53 +00:00
|
|
|
orderedViews.append(library["Id"])
|
2020-06-06 15:01:38 +00:00
|
|
|
return orderedViews
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def setConfiguration(self, userId: str, configuration: dict):
|
2020-06-06 15:01:38 +00:00
|
|
|
"""
|
|
|
|
Sets a user's configuration (Settings the user can change themselves).
|
|
|
|
:param userId: ID of the user to modify.
|
|
|
|
:param configuration: Configuration to write in dictionary form.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
resp = requests.post(
|
|
|
|
self.server + "/Users/" + userId + "/Configuration",
|
|
|
|
headers=self.header,
|
|
|
|
params=configuration,
|
|
|
|
)
|
|
|
|
if resp.status_code == 200 or resp.status_code == 204:
|
2020-06-06 15:01:38 +00:00
|
|
|
return True
|
|
|
|
elif resp.status_code == 401:
|
2020-06-21 19:29:53 +00:00
|
|
|
if hasattr(self, "username") and hasattr(self, "password"):
|
2020-06-06 15:01:38 +00:00
|
|
|
self.authenticate(self.username, self.password)
|
|
|
|
return self.setConfiguration(userId, configuration)
|
|
|
|
else:
|
|
|
|
raise self.AuthenticationRequiredError
|
|
|
|
else:
|
2020-06-06 17:06:25 +00:00
|
|
|
raise self.UnknownError
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def getConfiguration(self, username: str = "all", userId: str = "all"):
|
2020-06-06 17:06:25 +00:00
|
|
|
"""
|
|
|
|
Gets a user's Configuration. This can also be found in getUsers if
|
|
|
|
public is set to False.
|
|
|
|
:param username: The user's username.
|
2020-06-21 19:21:33 +00:00
|
|
|
:param userId: The user's ID.
|
2020-06-06 17:06:25 +00:00
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
return self.getUsers(username=username, userId=userId, public=False)[
|
|
|
|
"Configuration"
|
|
|
|
]
|
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def getDisplayPreferences(self, userId: str):
|
2020-06-06 17:06:25 +00:00
|
|
|
"""
|
|
|
|
Gets a user's Display Preferences (Home layout).
|
|
|
|
:param userId: The user's ID.
|
|
|
|
"""
|
2020-06-21 19:29:53 +00:00
|
|
|
resp = requests.get(
|
|
|
|
(
|
|
|
|
self.server
|
|
|
|
+ "/DisplayPreferences/usersettings"
|
|
|
|
+ "?userId="
|
|
|
|
+ userId
|
|
|
|
+ "&client=emby"
|
|
|
|
),
|
|
|
|
headers=self.header,
|
|
|
|
)
|
2020-06-06 17:06:25 +00:00
|
|
|
if resp.status_code == 200:
|
|
|
|
return resp.json()
|
|
|
|
elif resp.status_code == 401:
|
2020-06-21 19:29:53 +00:00
|
|
|
if hasattr(self, "username") and hasattr(self, "password"):
|
2020-06-06 17:06:25 +00:00
|
|
|
self.authenticate(self.username, self.password)
|
|
|
|
return self.getDisplayPreferences(userId)
|
|
|
|
else:
|
|
|
|
raise self.AuthenticationRequiredError
|
|
|
|
else:
|
|
|
|
raise self.UnknownError
|
2020-06-21 19:29:53 +00:00
|
|
|
|
2020-06-21 19:21:33 +00:00
|
|
|
def setDisplayPreferences(self, userId: str, preferences: dict):
|
2020-06-06 17:06:25 +00:00
|
|
|
"""
|
|
|
|
Sets a user's Display Preferences (Home layout).
|
|
|
|
:param userId: The user's ID.
|
|
|
|
:param preferences: The preferences to set.
|
|
|
|
"""
|
|
|
|
tempheader = self.header
|
2020-06-21 19:29:53 +00:00
|
|
|
tempheader["Content-type"] = "application/json"
|
|
|
|
resp = requests.post(
|
|
|
|
(
|
|
|
|
self.server
|
|
|
|
+ "/DisplayPreferences/usersettings"
|
|
|
|
+ "?userId="
|
|
|
|
+ userId
|
|
|
|
+ "&client=emby"
|
|
|
|
),
|
|
|
|
headers=tempheader,
|
|
|
|
json=preferences,
|
|
|
|
)
|
|
|
|
if resp.status_code == 200 or resp.status_code == 204:
|
2020-06-06 17:06:25 +00:00
|
|
|
return True
|
|
|
|
elif resp.status_code == 401:
|
2020-06-21 19:29:53 +00:00
|
|
|
if hasattr(self, "username") and hasattr(self, "password"):
|
2020-06-06 17:06:25 +00:00
|
|
|
self.authenticate(self.username, self.password)
|
|
|
|
return self.setDisplayPreferences(userId, preferences)
|
|
|
|
else:
|
|
|
|
raise self.AuthenticationRequiredError
|
|
|
|
else:
|
|
|
|
return resp
|