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 {}