import requests as RQ from requests.auth import HTTPBasicAuth from urllib.parse import urljoin, urlparse from fabric import Connection import time import json import base64 import io from datetime import datetime,timedelta from sshpubkeys import AuthorizedKeysFile from utils import genpw,handle_config from pprint import pprint """NOTES http://192.168.2.25:8080/sonarr/api/v3/release?seriesId=158&seasonNumber=8 http://192.168.2.25:8080/sonarr/api/v3/release?episodeId=12272 http://192.168.2.25:8080/radarr/api/v3/release?movieId=567 http://192.168.2.25:9000/api/endpoints/1/docker/containers/json?all=1&filters=%7B%22label%22:%5B%22com.docker.compose.project%3Dtvstack%22%5D%7D """ class Api(object): def __init__(self, url, **kwargs): self.url = url self.session= RQ.Session() for k, v in kwargs.items(): setattr(self, k, v) if hasattr(self, "login"): self.login() def get(self, endpoint, **kwargs): ret = self.session.get(urljoin(self.url, endpoint), **kwargs) ret.raise_for_status() return ret def post(self, endpoint, **kwargs): return self.session.post(urljoin(self.url, endpoint), **kwargs) class Portainer(object): def __init__(self, url, username, passwd): self.url = url self.session= RQ.Session() jwt = self.session.post( urljoin(self.url, "api/auth"), json={"username": passwd, "password": username}, ).json() self.session.headers.update({"Authorization": "Bearer {0[jwt]}".format(jwt)}) def containers(self, container_id=None): if container_id is None: res = self.session.get( urljoin(self.url, "api/endpoints/1/docker/containers/json"), params={ "all": 1, "filters": json.dumps( {"label": ["com.docker.compose.project=tvstack"]} ), }, ) else: res = self.session.get( urljoin( self.url, "api/endpoints/1/docker/containers/{}/json".format(container_id), ) ) res.raise_for_status() res = res.json() if container_id is None: for container in res: pass # print("Gettings stats for",container['Id']) # container['stats']=self.stats(container['Id']) # container['top']=self.top(container['Id']) else: res["stats"] = self.stats(container_id) res["top"] = self.top(container_id) return res def top(self, container_id): res = self.session.get( urljoin( self.url, "api/endpoints/1/docker/containers/{}/top".format(container_id), ) ) res.raise_for_status() res = res.json() cols = res["Titles"] ret = [] return res def stats(self, container_id): res = self.session.get( urljoin( self.url, "api/endpoints/1/docker/containers/{}/stats".format(container_id), ), params={"stream": False}, ) res.raise_for_status() return res.json() def test(self): self.containers() return {} class Jellyfin(object): def __init__(self, url, api_key): self.url = url self.session = RQ.Session() self.session.headers.update({"X-Emby-Token": api_key}) self.api_key = api_key self.user_id = self.get_self()['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" ]) # auth = 'MediaBrowser Client="MediaDash", Device="Python", DeviceId="{}", Version="{}", Token="{}"'.format( # self.device_id, RQ.__version__, self.api_key # ) # self.session.headers.update({"X-Emby-Authorization": auth}) 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 chapter_image_url(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.session.get(urljoin(self.url, "Sessions")) res.raise_for_status() return res.json() def media_info(self,item_id): res = self.session.get(urljoin(self.url, "Users",self.user_id,"Items",item_id)) res.raise_for_status() return res.json() def system_info(self): res = self.session.get(urljoin(self.url, "System/Info")) res.raise_for_status() return res.json() def __get_child_items(self, item_id): print(item_id) res = self.session.get( urljoin(self.url, "Users",self.user_id,"Items"), 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_library(self): res = self.session.get(urljoin(self.url, "Library/MediaFolders")) res.raise_for_status() for folder in res.json().get("Items", []): for item in self.get_recursive(folder["Id"]): pass 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.session.get(urljoin(self.url, "users","me")) res.raise_for_status() return res.json()[0] def get_users(self): res=self.session.get(urljoin(self.url, "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 {} class QBittorrent(object): status_map = { "downloading": ("Downloading", "primary"), "uploading": ("Seeding", "success"), "forcedDL": ("Downloading [Forced]", "primary"), "forcedUP": ("Seeding [Forced]", "success"), "pausedDL": ("Downloading [Paused]", "secondary"), "pausedUP": ("Seeding [Paused]", "secondary"), "stalledDL": ("Downloading [Stalled]", "warning"), "stalledUP": ("Seeding [Stalled]", "warning"), "metaDL": ("Downloading metadata", "primary"), "error": ("Error", "danger"), "missingFiles": ("Missing Files", "danger"), "queuedUP": ("Seeding [Queued]", "info"), "queuedDL": ("Downloading [Queued]", "info"), } tracker_status = { 0: ("Disabled", "secondary"), 1: ("Not contacted", "info"), 2: ("Working", "success"), 3: ("Updating", "warning"), 4: ("Not working", "danger"), } def __init__(self, url, username, passwd): self.url = url self.username = username self.passwd = passwd self.rid = int(time.time()) self.session= RQ.Session() url = urljoin(self.url, "/api/v2/auth/login") self.session.post( url, data={"username": self.username, "password": self.passwd} ).raise_for_status() def get(self, url, **kwargs): kwargs["rid"] = self.rid url = urljoin(self.url, url) res = self.session.get(url, params=kwargs) res.raise_for_status() try: return res.json() except ValueError: return res.text def add(self, **kwargs): self.rid += 1 url = urljoin(self.url, "/api/v2/torrents/add") ret = self.session.post(url, data=kwargs) return ret.text, ret.status_code def add_trackers(self, infohash, trackers=None): if trackers is None: trackers = [] for tracker_list in [ "https://newtrackon.com/api/live", "https://ngosang.github.io/trackerslist/trackers_best.txt", ]: try: trackers_res = RQ.get(tracker_list) trackers_res.raise_for_status() except Exception as e: print("Error getting tracker list:", e) continue trackers += trackers_res.text.split() url = urljoin(self.url, "/api/v2/torrents/addTrackers") data = {"hash": infohash, "urls": "\n\n".join(trackers)} ret = self.session.post(url, data=data) ret.raise_for_status() return ret.text def poll(self, infohash=None): if infohash: ret = {} res = self.get("/api/v2/torrents/info", hashes=infohash) ret["info"] = res for endpoint in ["properties", "trackers", "webseeds", "files"]: url = "/api/v2/torrents/{}".format(endpoint) res = self.get("/api/v2/torrents/{}".format(endpoint), hash=infohash) if endpoint == "trackers": for v in res: if v["tier"] == "": v["tier"] = -1 v["status"] = self.tracker_status.get( v["status"], ("Unknown", "light") ) v["total_peers"] = ( v["num_seeds"] + v["num_leeches"] + v["num_peers"] ) for k in [ "num_seeds", "num_leeches", "total_peers", "num_downloaded", "num_peers", ]: if v[k] < 0: v[k] = (-1, "?") else: v[k] = (v[k], v[k]) ret[endpoint] = res ret["info"] = ret["info"][0] ret["info"]["state"] = self.status_map.get( ret["info"]["state"], (ret["info"]["state"], "light") ) for tracker in ret["trackers"]: tracker["name"] = urlparse(tracker["url"]).netloc or tracker["url"] tracker["has_url"] = bool(urlparse(tracker["url"]).netloc) return ret res = self.get("/api/v2/sync/maindata") if "torrents" in res: for k, v in res["torrents"].items(): v["hash"] = k v["speed"] = v["upspeed"] + v["dlspeed"] dl_rate = v["downloaded"] / max(0, time.time() - v["added_on"]) if dl_rate > 0: v["eta"] = max(0, (v["size"] - v["downloaded"]) / dl_rate) else: v["eta"] = 0 if v["time_active"]==0: dl_rate=0 else: dl_rate = v["downloaded"] / v["time_active"] if dl_rate > 0: v["eta_act"] = max(0, (v["size"] - v["downloaded"]) / dl_rate) else: v["eta_act"] = 0 res["torrents"][k] = v res["version"] = self.get("/api/v2/app/version") self.rid = res["rid"] return res def status(self, infohash=None): self.rid += 1 return self.poll(infohash) def peer_log(self, limit=0): return self.get("/api/v2/log/peers")[-limit:] def log(self, limit=0): return self.get("/api/v2/log/main")[-limit:] def test(self): self.poll() return {} class Radarr(object): def __init__(self, url, api_key): self.url = url self.api_key = api_key def get(self, url, **kwargs): kwargs["apikey"] = self.api_key kwargs["_"] = str(int(time.time())) res = RQ.get(urljoin(self.url, url), params=kwargs) res.raise_for_status() try: return res.json() except: return res.text def search(self, query): return self.get("api/v3/movie/lookup", term=query) def status(self): return self.get("api/v3/system/status") def history(self, pageSize=500): return self.get( "api/v3/history", page=1, pageSize=500, sortDirection="descending", sortKey="date", ) def calendar(self,days=30): today=datetime.today() start=today-timedelta(days=days) end=today+timedelta(days=days) return self.get("api/v3/calendar",unmonitored=False,start=start.isoformat(),end=end.isoformat()) def movies(self): return self.get("api/v3/movie") def queue(self, series_id): return self.get("api/v3/queue") def log(self, limit=0): return self.get( "api/v3/log", page=1, pageSize=(limit or 1024), sortDirection="descending", sortKey="time", ) def test(self): self.status() return {} class Sonarr(object): def __init__(self, url, api_key): self.url = url self.api_key = api_key def get(self, url, **kwargs): kwargs["apikey"] = self.api_key kwargs["_"] = str(int(time.time())) res = RQ.get(urljoin(self.url, url), params=kwargs) res.raise_for_status() try: return res.json() except: return res.text def search(self, query): return self.get("api/v3/series/lookup", term=query) def status(self): return self.get("api/v3/system/status") def history(self, pageSize=500): return self.get( "api/v3/history", page=1, pageSize=500, sortDirection="descending", sortKey="date", ) def calendar(self,days=30): today=datetime.today() start=today-timedelta(days=days) end=today+timedelta(days=days) return self.get("api/v3/calendar",unmonitored=False,start=start.isoformat(),end=end.isoformat()) def series(self, series_id=None): if series_id is None: return self.get("api/v3/series") ret = {} ret["episodes"] = self.get("api/v3/episode", seriesId=series_id) ret["episodeFile"] = self.get("api/v3/episodeFile", seriesId=series_id) ret["queue"] = self.get("api/v3/queue/details", seriesId=series_id) return ret def queue(self, series_id): return self.get("api/v3/queue") def episodes(self, series_id): return self.get("api/v3/episode", seriesId=series_id) def log(self, limit=0): return self.get( "api/v3/log", page=1, pageSize=(limit or 1024), sortDirection="descending", sortKey="time", ) def test(self): self.status() return {} class Jackett(object): def __init__(self, url, api_key): self.url = url self.api_key = api_key self.session= RQ.Session() self.session.post("http://192.168.2.25:9117/jackett/UI/Dashboard") def search(self, query, indexers=None): params = {"apikey": self.api_key, "Query": query, "_": str(int(time.time()))} if indexers: params["Tracker[]"] = indexers res = self.session.get( urljoin(self.url, f"api/v2.0/indexers/all/results"), params=params ) res.raise_for_status() res = res.json() for val in res["Results"]: for prop in ["Gain", "Seeders", "Peers", "Grabs", "Files"]: val[prop] = val.get(prop) or 0 return res def indexers(self): return [ (t["id"], t["name"]) for t in self.session.get(urljoin(self.url, "api/v2.0/indexers")).json() if t.get("configured") ] def test(self): errors = {} for idx, name in self.indexers(): print("Testing indexer", name) result = self.session.post( urljoin(self.url, "api/v2.0/indexers/{}/test".format(idx)) ) if result.text: errors[name] = result.json()["error"] return errors class Client(object): def __init__(self, cfg): self.cfg = cfg self.jackett = Jackett(cfg["jackett_url"], cfg["jackett_api_key"]) self.sonarr = Sonarr(cfg["sonarr_url"], cfg["sonarr_api_key"]) self.radarr = Radarr(cfg["radarr_url"], cfg["radarr_api_key"]) self.jellyfin = Jellyfin( cfg["jellyfin_url"], cfg["jellyfin_api_key"] ) self.qbittorent = QBittorrent( cfg["qbt_url"], cfg["qbt_username"], cfg["qbt_passwd"] ) self.portainer = Portainer( cfg["portainer_url"], cfg["portainer_username"], cfg["portainer_passwd"] ) self.ssh = Connection('root@server') def _get_ssh_keys(self): cfg = handle_config() res=self.ssh.get("/data/.ssh/authorized_keys",io.BytesIO()) res.local.seek(0) ret=[] for line in str(res.local.read(),"utf8").splitlines(): if line.startswith("#"): continue else: key_type,key,comment=line.split(None,2) ret.append((key_type,key,comment)) return ret def add_user(self,name,ssh_key): cfg = handle_config() user_config = cfg['jellyfin_user_config'] user_policy = cfg['jellyfin_user_policy'] passwd = genpw() res=self.ssh.get("/data/.ssh/authorized_keys",io.BytesIO()) res.local.seek(0) keys=[l.split(None,2) for l in str(res.local.read(),"utf8").splitlines()] key_type,key,*_=ssh_key.split() keys.append([key_type,key,name]) new_keys=[] seen_keys=set() for key_type,key,name in keys: if key not in seen_keys: seen_keys.add(key) new_keys.append([key_type,key,name]) new_keys_file="\n".join(" ".join(key) for key in new_keys) self.ssh.put(io.BytesIO(bytes(new_keys_file,"utf8")),"/data/.ssh/authorized_keys",preserve_mode=False) user = self.jellyfin.post("Users/New", json={"Name": name, "Password": passwd}) user.raise_for_status() user = user.json() self.jellyfin.post("Users/{Id}/Configuration".format(**user), json=user_config).raise_for_status() self.jellyfin.post("Users/{Id}/Policy".format(**user), json=user_policy).raise_for_status() return passwd @staticmethod def test(cfg=None): cfg = cfg or self.cfg modules = [ ( "Jackett", lambda cfg: Jackett(cfg["jackett_url"], cfg["jackett_api_key"]), ), ("Sonarr", lambda cfg: Sonarr(cfg["sonarr_url"], cfg["sonarr_api_key"])), ("Radarr", lambda cfg: Radarr(cfg["radarr_url"], cfg["radarr_api_key"])), ( "QBittorrent", lambda cfg: QBittorrent( cfg["qbt_url"], cfg["qbt_username"], cfg["qbt_passwd"] ), ), ( "Jellyfin", lambda cfg: Jellyfin( cfg["jellyfin_url"], cfg["jellyfin_username"], cfg["jellyfin_passwd"], ), ), ( "Portainer", lambda cfg: Portainer( cfg["portainer_url"], cfg["portainer_username"], cfg["portainer_passwd"], ), ), ] errors = {} success = True for mod, Module in modules: try: print("Testing", mod) errors[mod] = Module(cfg).test() if errors[mod]: success = False except Exception as e: print(dir(e)) errors[mod] = str(e) success = False print(errors) return {"success": success, "errors": errors}