334 lines
11 KiB
Python
334 lines
11 KiB
Python
|
import time
|
||
|
import base64
|
||
|
from urllib.parse import urljoin
|
||
|
from datetime import timedelta
|
||
|
|
||
|
import requests as RQ
|
||
|
from dateutil.parser import parse as parse_datetime
|
||
|
|
||
|
from utils import timed_cache
|
||
|
|
||
|
|
||
|
class Jellyfin(object):
|
||
|
def __init__(self, url, user, password):
|
||
|
self.url = url
|
||
|
self.session = RQ.Session()
|
||
|
self.device_id = str(
|
||
|
base64.b64encode(
|
||
|
"MediaDash ({})".format(
|
||
|
self.session.headers["User-Agent"]).encode("utf-8")),
|
||
|
"utf8",
|
||
|
)
|
||
|
self.auth_headers = {
|
||
|
"X-Emby-Authorization": 'MediaBrowser Client="MediaDash", Device="Python", DeviceId="{}", Version="{}"'.format(
|
||
|
self.device_id, RQ.__version__)}
|
||
|
self.user = None
|
||
|
if user is not None:
|
||
|
res = self.login_user(user, password)
|
||
|
self.api_key = res["AccessToken"]
|
||
|
else:
|
||
|
self.api_key = password
|
||
|
self.auth_headers = {
|
||
|
"X-Emby-Authorization": 'MediaBrowser Client="MediaDash", Device="Python", DeviceId="{}", Version="{}", Token="{}"'.format(
|
||
|
self.device_id, RQ.__version__, self.api_key)}
|
||
|
# ws_url=self.url.replace("http","ws").rstrip("/")+"/?"+urlencode({"api_key":self.api_key,"deviceId":self.device_id})
|
||
|
# self.ws = websocket.WebSocketApp(ws_url,on_open=print,on_error=print,on_message=print,on_close=print)
|
||
|
# self.ws_thread = Thread(target=self.ws.run_forever,daemon=True)
|
||
|
self.session.headers.update(
|
||
|
{**self.auth_headers, "X-Emby-Token": self.api_key})
|
||
|
self.user = self.get_self()
|
||
|
self.user_id = self.user["Id"]
|
||
|
self.playstate_commands = sorted(
|
||
|
[
|
||
|
"Stop",
|
||
|
"Pause",
|
||
|
"Unpause",
|
||
|
"NextTrack",
|
||
|
"PreviousTrack",
|
||
|
"Seek",
|
||
|
"Rewind",
|
||
|
"FastForward",
|
||
|
"PlayPause",
|
||
|
]
|
||
|
)
|
||
|
self.session_commands = sorted(
|
||
|
[
|
||
|
"MoveUp",
|
||
|
"MoveDown",
|
||
|
"MoveLeft",
|
||
|
"MoveRight",
|
||
|
"PageUp",
|
||
|
"PageDown",
|
||
|
"PreviousLetter",
|
||
|
"NextLetter",
|
||
|
"ToggleOsd",
|
||
|
"ToggleContextMenu",
|
||
|
"Select",
|
||
|
"Back",
|
||
|
"TakeScreenshot",
|
||
|
"SendKey",
|
||
|
"SendString",
|
||
|
"GoHome",
|
||
|
"GoToSettings",
|
||
|
"VolumeUp",
|
||
|
"VolumeDown",
|
||
|
"Mute",
|
||
|
"Unmute",
|
||
|
"ToggleMute",
|
||
|
"SetVolume",
|
||
|
"SetAudioStreamIndex",
|
||
|
"SetSubtitleStreamIndex",
|
||
|
"ToggleFullscreen",
|
||
|
"DisplayContent",
|
||
|
"GoToSearch",
|
||
|
"DisplayMessage",
|
||
|
"SetRepeatMode",
|
||
|
"ChannelUp",
|
||
|
"ChannelDown",
|
||
|
"Guide",
|
||
|
"ToggleStats",
|
||
|
"PlayMediaSource",
|
||
|
"PlayTrailers",
|
||
|
"SetShuffleQueue",
|
||
|
"PlayState",
|
||
|
"PlayNext",
|
||
|
"ToggleOsdMenu",
|
||
|
"Play",
|
||
|
]
|
||
|
)
|
||
|
|
||
|
def login_user(self, user, passwd):
|
||
|
res = self.post(
|
||
|
"Users/AuthenticateByName",
|
||
|
json={"Username": user, "Pw": passwd},
|
||
|
headers=self.auth_headers,
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()
|
||
|
self.session.headers.update(
|
||
|
{**self.auth_headers, "X-Emby-Token": res["AccessToken"]}
|
||
|
)
|
||
|
return res
|
||
|
|
||
|
def logout(self):
|
||
|
self.session.post(urljoin(self.url, "Sessions/Logout"))
|
||
|
|
||
|
def status(self):
|
||
|
res = self.session.get(urljoin(self.url, "System/Info"))
|
||
|
res.raise_for_status()
|
||
|
return res.json()
|
||
|
|
||
|
def chapter_image_url(self, item_id, chapter_num, tag):
|
||
|
return urljoin(
|
||
|
self.url,
|
||
|
"Items",
|
||
|
item_id,
|
||
|
"Images",
|
||
|
"Chapter",
|
||
|
chapter_num)
|
||
|
|
||
|
def rq(self, method, url, *args, **kwargs):
|
||
|
res = self.session.request(
|
||
|
method, urljoin(
|
||
|
self.url, url), *args, **kwargs)
|
||
|
res.raise_for_status()
|
||
|
return res
|
||
|
|
||
|
def get(self, url, *args, **kwargs):
|
||
|
res = self.session.get(urljoin(self.url, url), *args, **kwargs)
|
||
|
res.raise_for_status()
|
||
|
return res
|
||
|
|
||
|
def post(self, url, *args, **kwargs):
|
||
|
res = self.session.post(urljoin(self.url, url), *args, **kwargs)
|
||
|
res.raise_for_status()
|
||
|
return res
|
||
|
|
||
|
def sessions(self):
|
||
|
res = self.get("Sessions")
|
||
|
res.raise_for_status()
|
||
|
return res.json()
|
||
|
|
||
|
@timed_cache()
|
||
|
def season_episodes(self, item_id, season_id):
|
||
|
res = self.get(
|
||
|
"Shows/{}/Episodes".format(item_id),
|
||
|
params={
|
||
|
"UserId": self.user_id,
|
||
|
"seasonId": season_id,
|
||
|
"fields": "Overview,MediaStreams,MediaSources,ExternalUrls",
|
||
|
},
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()["Items"]
|
||
|
for episode in res:
|
||
|
episode["Info"] = self.media_info(episode["Id"])
|
||
|
return res
|
||
|
|
||
|
@timed_cache()
|
||
|
def seasons(self, item_id):
|
||
|
res = self.get(
|
||
|
"Shows/{}/Seasons".format(item_id),
|
||
|
params={
|
||
|
"UserId": self.user_id,
|
||
|
"fields": "Overview,MediaStreams,MediaSources,ExternalUrls",
|
||
|
},
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()["Items"]
|
||
|
for season in res:
|
||
|
season["Episodes"] = self.season_episodes(item_id, season["Id"])
|
||
|
return res
|
||
|
|
||
|
@timed_cache()
|
||
|
def media_info(self, item_id):
|
||
|
res = self.get(
|
||
|
"Users/{}/Items/{}".format(self.user_id, item_id),
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()
|
||
|
if res["Type"] == "Series":
|
||
|
res["Seasons"] = self.seasons(item_id)
|
||
|
return res
|
||
|
|
||
|
def system_info(self):
|
||
|
res = self.get("System/Info")
|
||
|
res.raise_for_status()
|
||
|
return res.json()
|
||
|
|
||
|
def __get_child_items(self, item_id):
|
||
|
print(item_id)
|
||
|
res = self.get(
|
||
|
"Users/{}/Items".format(self.user_id),
|
||
|
params={"ParentId": item_id},
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
return res.json()
|
||
|
|
||
|
def get_recursive(self, item_id):
|
||
|
for item in self.__get_child_items(item_id).get("Items", []):
|
||
|
yield item
|
||
|
yield from self.get_recursive(item["Id"])
|
||
|
|
||
|
def get_counts(self):
|
||
|
res = self.get("Items/Counts").json()
|
||
|
return res
|
||
|
|
||
|
@timed_cache(seconds=10)
|
||
|
def id_map(self):
|
||
|
res = self.get(
|
||
|
"Users/{}/Items".format(self.user_id),
|
||
|
params={
|
||
|
"recursive": True,
|
||
|
"includeItemTypes": "Movie,Series",
|
||
|
"collapseBoxSetItems": False,
|
||
|
"fields": "ProviderIds",
|
||
|
},
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()["Items"]
|
||
|
id_map = {}
|
||
|
for item in res:
|
||
|
for _, prov_id in item["ProviderIds"].items():
|
||
|
for prov in ["Imdb", "Tmdb", "Tvdb"]:
|
||
|
id_map[(prov.lower(), prov_id)] = item["Id"]
|
||
|
return id_map
|
||
|
|
||
|
@timed_cache()
|
||
|
def get_library(self):
|
||
|
res = self.get(
|
||
|
"Users/{}/Items".format(self.user_id),
|
||
|
params={
|
||
|
"recursive": True,
|
||
|
"includeItemTypes": "Movie,Series",
|
||
|
"collapseBoxSetItems": False,
|
||
|
},
|
||
|
).json()
|
||
|
library = {}
|
||
|
for item in res["Items"]:
|
||
|
library[item["Id"]] = item
|
||
|
for item in res["Items"]:
|
||
|
for key, value in item.copy().items():
|
||
|
if key != "Id" and key.endswith("Id"):
|
||
|
key = key[:-2]
|
||
|
if value in library and key not in item:
|
||
|
item[key] = library[value]
|
||
|
return library
|
||
|
|
||
|
def get_usage(self):
|
||
|
report = self.post(
|
||
|
"user_usage_stats/submit_custom_query",
|
||
|
params={"stamp": int(time.time())},
|
||
|
json={
|
||
|
"CustomQueryString": "SELECT * FROM PlaybackActivity",
|
||
|
"ReplaceUserId": True,
|
||
|
},
|
||
|
).json()
|
||
|
ret = []
|
||
|
for row in report["results"]:
|
||
|
rec = dict(zip(report["colums"], row))
|
||
|
rec["PlayDuration"] = timedelta(seconds=int(rec["PlayDuration"]))
|
||
|
ts = rec.pop("DateCreated")
|
||
|
if ts:
|
||
|
rec["Timestamp"] = parse_datetime(ts)
|
||
|
ret.append(rec)
|
||
|
return ret
|
||
|
|
||
|
def __db_fetch(self, endpoint):
|
||
|
ret = []
|
||
|
res = self.session.get(
|
||
|
urljoin(
|
||
|
self.url,
|
||
|
endpoint),
|
||
|
params={
|
||
|
"StartIndex": 0,
|
||
|
"IncludeItemTypes": "*",
|
||
|
"ReportColumns": ""},
|
||
|
)
|
||
|
res.raise_for_status()
|
||
|
res = res.json()
|
||
|
headers = [h["Name"].lower() for h in res["Headers"]]
|
||
|
for row in res["Rows"]:
|
||
|
fields = [c["Name"] for c in row["Columns"]]
|
||
|
ret.append(dict(zip(headers, fields)))
|
||
|
ret[-1]["row_type"] = row["RowType"]
|
||
|
return ret
|
||
|
|
||
|
def get_self(self):
|
||
|
res = self.get("Users/Me")
|
||
|
return res.json()
|
||
|
|
||
|
def get_users(self, user_id=None):
|
||
|
if user_id:
|
||
|
res = self.get("Users/{}".format(user_id))
|
||
|
else:
|
||
|
res = self.get("Users")
|
||
|
res.raise_for_status()
|
||
|
return res.json()
|
||
|
|
||
|
def activity(self):
|
||
|
return self.__db_fetch("Reports/Activities")
|
||
|
|
||
|
def report(self):
|
||
|
return self.__db_fetch("Reports/Items")
|
||
|
|
||
|
def stop_session(self, session_id):
|
||
|
sessions = self.get("Sessions").json()
|
||
|
for session in sessions:
|
||
|
if session["Id"] == session_id and "NowPlayingItem" in session:
|
||
|
s_id = session["Id"]
|
||
|
u_id = session["UserId"]
|
||
|
i_id = session["NowPlayingItem"]["Id"]
|
||
|
d_id = session["DeviceId"]
|
||
|
self.rq(
|
||
|
"delete",
|
||
|
"Videos/ActiveEncodings",
|
||
|
params={"deviceId": d_id, "playSessionId": s_id},
|
||
|
)
|
||
|
self.rq("delete", f"Users/{u_id}/PlayingItems/{i_id}")
|
||
|
self.rq("post", f"Sessions/{s_id}/Playing/Stop")
|
||
|
|
||
|
def test(self):
|
||
|
self.status()
|
||
|
return {}
|