MediaDash/api.py

650 lines
23 KiB
Python

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}