push latest changes

This commit is contained in:
Daniel S. 2021-12-13 19:11:43 +01:00
parent 7523a19d1f
commit cb2b5c2c2b
63 changed files with 3158 additions and 1552 deletions

24
views/__init__.py Normal file
View file

@ -0,0 +1,24 @@
import sys
from flask import Blueprint
from .api import api # noqa
from .config import config_page # noqa
from .containers import containers_page # noqa
from .history import history_page # noqa
from .home import home_page # noqa
from .jellyfin import jellyfin_page # noqa
from .logs import logs_page # noqa
from .qbittorrent import qbittorrent_page # noqa
from .radarr import radarr_page # noqa
from .remote import remote_page # noqa
from .requests import requests_page # noqa
from .search import search_page # noqa
from .sonarr import sonarr_page # noqa
from .transcode import transcode_page # noqa
def register_blueprints(app):
for k, v in vars(sys.modules[__name__]).items():
if isinstance(v, Blueprint):
app.register_blueprint(v)

32
views/api/__init__.py Normal file
View file

@ -0,0 +1,32 @@
from flask import Blueprint, request, flash, session, redirect, url_for
import time
from api import Client
from utils import admin_required
api = Blueprint("api", __name__, url_prefix="/api")
@api.route("/add_torrent", methods=["POST"])
@admin_required
def add_torrent():
category = request.form.get("category")
c = Client()
hashes_1 = set(c.qbittorent.status().get("torrents", {}))
links = ""
count = 0
for link in request.form.getlist("torrent[]"):
links += link + "\n"
count += 1
c.qbittorent.add(urls=links, category=category)
for _ in range(10):
status = c.qbittorent.status().get("torrents", {})
hashes_2 = set(status)
if len(hashes_2 - hashes_1) == count:
break
time.sleep(0.5)
else:
flash("Some torrents failed to get added to QBittorrent", "waring")
new_torrents = sorted(hashes_2 - hashes_1)
session["new_torrents"] = {h: status[h] for h in new_torrents}
return redirect(url_for("search"))

45
views/config/__init__.py Normal file
View file

@ -0,0 +1,45 @@
from flask import Blueprint, json, render_template, request
from api import Client
from forms import ConfigForm
from utils import admin_required, handle_config, populate_form, validate_transcoding_profiles
config_page = Blueprint("config", __name__, url_prefix="/config")
@config_page.route("/", methods=["GET", "POST"])
@admin_required
def index():
form = ConfigForm()
cfg = {}
populate_form(form)
if form.validate_on_submit():
skip = ["save", "test", "csrf_token"]
transcode_profiles = request.files.get("transcode_profiles")
if transcode_profiles:
try:
form.transcode_profiles.data = json.load(transcode_profiles)
validate_transcoding_profiles(form.transcode_profiles.data)
except ValueError as e:
form.transcode_profiles.data = None
form.transcode_profiles.errors = [
"Invalid json data in file {}: {}".format(
transcode_profiles.filename, e
)
]
else:
form.transcode_profiles.data = handle_config().get("transcode_profiles", {})
if form.errors:
return render_template("config.html", form=form)
for name, field in form._fields.items():
if name in skip:
continue
cfg[name] = field.data
if form.test.data:
test_res = Client.test(cfg)
populate_form(form, cfg)
return render_template("config.html", form=form, test=test_res)
handle_config(cfg)
populate_form(form)
return render_template("config.html", form=form)
form.process()
return render_template("config.html", form=form)

View file

@ -0,0 +1,22 @@
from flask import Blueprint, render_template
from api import Client
from utils import admin_required
containers_page = Blueprint("containers", __name__, url_prefix="/containers")
@containers_page.route("/")
@admin_required
def index():
c = Client()
containers = c.portainer.containers()
return render_template("containers/index.html", containers=containers)
@containers_page.route("/<container_id>")
@admin_required
def details(container_id):
c = Client()
container = c.portainer.containers(container_id)
return render_template("containers/details.html", container=container)

15
views/history/__init__.py Normal file
View file

@ -0,0 +1,15 @@
from flask import Blueprint, render_template
from api import Client
from utils import admin_required
history_page = Blueprint("history", __name__, url_prefix="/history")
@history_page.route("/")
@admin_required
def index():
c = Client()
sonarr = c.sonarr.history()
radarr = c.radarr.history()
return render_template("history.html", sonarr=sonarr, radarr=radarr)

16
views/home/__init__.py Normal file
View file

@ -0,0 +1,16 @@
from flask import Blueprint, render_template
from flask_login import current_user
import stats_collect
home_page = Blueprint("home", __name__)
@home_page.route("/")
def index():
stats = stats_collect.Stats()
if not (current_user.is_authenticated and current_user.is_admin):
stats["images"] = [
img for img in stats["images"] if img[0] != "Torrents"]
return render_template("index.html", fluid=True, data=stats)

View file

@ -0,0 +1,43 @@
from flask import Blueprint, render_template
from flask_login import current_user
import stats_collect
from api import Client
from utils import login_required
jellyfin_page = Blueprint("jellyfin", __name__, url_prefix="/jellyfin")
@jellyfin_page.route("/")
@login_required
def index():
c = Client()
stats = stats_collect.Stats()
jellyfin = {
"info": c.jellyfin.system_info(),
"status": c.jellyfin.status(),
"counts": c.jellyfin.get_counts(),
"library": stats["library"],
}
if not (current_user.is_authenticated and current_user.is_admin):
jellyfin["library"] = {}
jellyfin["status"]["HasUpdateAvailable"] = False
jellyfin["status"]["HasPendingRestart"] = False
return render_template("jellyfin/index.html", **jellyfin)
@jellyfin_page.route("/<item_id>")
@login_required
def details(item_id):
c = Client()
jellyfin = {
"info": c.jellyfin.system_info(),
"status": c.jellyfin.status(),
"item": c.jellyfin.media_info(item_id),
}
if jellyfin["item"].get("Type") == "Movie":
return render_template("jellyfin/movie.html", **jellyfin)
if jellyfin["item"].get("Type") == "Series":
return render_template("jellyfin/series.html", **jellyfin)
return render_template("jellyfin/details.html", **jellyfin)

19
views/logs/__init__.py Normal file
View file

@ -0,0 +1,19 @@
from flask import Blueprint, render_template
from api import Client
from utils import admin_required
logs_page = Blueprint("log", __name__, url_prefix="/log")
@logs_page.route("/")
@admin_required
def index():
c = Client()
logs = {
"radarr": c.radarr.log(),
"sonarr": c.sonarr.log(),
"qbt": c.qbittorent.log(),
"peers": c.qbittorent.peer_log(),
}
return render_template("logs.html", logs=logs)

View file

@ -0,0 +1,48 @@
from flask import Blueprint, render_template, request, redirect
from api import Client
from utils import admin_required
qbittorrent_page = Blueprint(
"qbittorrent",
__name__,
url_prefix="/qbittorrent")
@qbittorrent_page.route("/")
@admin_required
def index():
c = Client()
qbt = c.qbittorent.status()
sort_by_choices = {
"speed": "Transfer Speed",
"eta": "Time remaining",
"state": "State",
"category": "Category",
}
return render_template(
"qbittorrent/index.html",
qbt=qbt,
status_map=c.qbittorent.status_map,
state_filter=request.args.get("state"),
sort_by=request.args.get("sort", "speed"),
sort_by_choices=sort_by_choices,
)
@qbittorrent_page.route("/add_trackers/<infohash>")
@admin_required
def add_trackers(infohash):
c = Client()
c.qbittorent.add_trackers(infohash)
return redirect(url_for("qbittorrent_details", infohash=infohash))
@qbittorrent_page.route("/<infohash>")
@admin_required
def details(infohash):
c = Client()
qbt = c.qbittorent.status(infohash)
return render_template(
"qbittorrent/details.html", qbt=qbt, status_map=c.qbittorent.status_map
)

24
views/radarr/__init__.py Normal file
View file

@ -0,0 +1,24 @@
from flask import Blueprint, render_template
from api import Client
from utils import admin_required
radarr_page = Blueprint("radarr", __name__, url_prefix="/radarr")
@radarr_page.route("/")
@admin_required
def index():
c = Client()
movies = c.radarr.movies()
status = c.radarr.status()
history = c.radarr.history()
return render_template(
"radarr/index.html", movies=movies, status=status, history=history
)
@radarr_page.route("/<movie_id>")
@admin_required
def details(movie_id):
return render_template("radarr/details.html")

98
views/remote/__init__.py Normal file
View file

@ -0,0 +1,98 @@
import base64
import hashlib
import io
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_ssh_public_key
from flask import Blueprint, redirect, render_template, request, url_for, Markup, flash
from forms import AddSSHUser
from api import Client
from utils import admin_required
remote_page = Blueprint("remote", __name__, url_prefix="/remote")
def ssh_fingerprint(key):
fp = hashlib.md5(base64.b64decode(key)).hexdigest()
return ":".join(a + b for a, b in zip(fp[::2], fp[1::2]))
@remote_page.route("/stop")
@admin_required
def stop():
c = Client()
session_id = request.args.get("session")
c.jellyfin.stop_session(session_id)
return redirect(url_for("remote.index"))
@remote_page.route("/")
@admin_required
def index():
c = Client()
res = c.ssh.get("/data/.ssh/authorized_keys", io.BytesIO())
res.local.seek(0)
ssh_keys = []
for key in str(res.local.read(), "utf8").splitlines():
disabled = False
if key.startswith("#"):
key = key.lstrip("#").lstrip()
disabled = True
load_ssh_public_key(bytes(key, "utf8"))
key_type, key, name = key.split(None, 2)
ssh_keys.append(
{
"disabled": disabled,
"type": key_type,
"key": key,
"fingerprint": ssh_fingerprint(key),
"name": name,
}
)
key = request.args.get("key")
enabled = request.args.get("enabled")
if not (key is None or enabled is None):
key_file = []
for ssh_key in ssh_keys:
if ssh_key["key"] == key:
ssh_key["disabled"] = enabled == "False"
if ssh_key["disabled"]:
key_file.append("#{type} {key} {name}".format(**ssh_key))
else:
key_file.append("{type} {key} {name}".format(**ssh_key))
buf = io.BytesIO(bytes("\n".join(key_file), "utf8"))
c.ssh.put(buf, "/data/.ssh/authorized_keys", preserve_mode=False)
return redirect(url_for("remote"))
jellyfin = {
"users": c.jellyfin.get_users(),
"sessions": c.jellyfin.sessions(),
"info": c.jellyfin.system_info(),
}
return render_template(
"remote/index.html",
ssh=ssh_keys,
jellyfin=jellyfin)
@remote_page.route("/add", methods=["GET", "POST"])
@admin_required
def add():
form = AddSSHUser()
c = Client()
if form.validate_on_submit():
key = load_ssh_public_key(bytes(form.data["ssh_key"], "utf8"))
rawKeyData = key.public_bytes(
encoding=serialization.Encoding.OpenSSH,
format=serialization.PublicFormat.OpenSSH,
)
passwd = c.add_user(form.data["name"], str(rawKeyData, "utf8"))
flash(
Markup(
"".join(
[
f"<p>Name: <b>{form.data['name']}</b></p>",
f"<p>PW: <b>{passwd}</b></p>",
f"<p>FP: <b>{ssh_fingerprint(rawKeyData.split()[1])}</b></p>",
])))
return render_template("remote/add.html", form=form)

217
views/requests/__init__.py Normal file
View file

@ -0,0 +1,217 @@
import base64
import json
from datetime import datetime
from flask import (
Blueprint,
current_app,
flash,
redirect,
render_template,
request,
session,
url_for,
)
from flask_login import current_user
from collections import defaultdict
from api import Client
from forms import RequestForm
from models import RequestItem, RequestUser
from utils import login_required, handle_config
requests_page = Blueprint("requests", __name__, url_prefix="/requests")
@requests_page.route("/<request_id>/<download_id>", methods=["GET"])
@login_required
def download_details(request_id, download_id):
c = Client()
request = RequestItem.query.filter(RequestItem.id == request_id).one_or_none()
if request is None:
flash("Unknown request ID", "danger")
return redirect(url_for("requests.details", request_id=request_id))
try:
qbt = c.qbittorent.poll(download_id)
except Exception:
flash("Unknown download ID", "danger")
return redirect(url_for("requests.details", request_id=request_id))
return render_template("qbittorrent/details.html", qbt=qbt)
@requests_page.route("/<request_id>", methods=["GET"])
@login_required
def details(request_id):
c = Client()
if current_user.is_admin:
requests = RequestItem.query
else:
user_id = current_user.get_id()
requests = RequestItem.query.filter(
RequestItem.users.any(RequestUser.user_id == user_id)
)
request = requests.filter(RequestItem.id == request_id).one_or_none()
if request is None:
flash("Unknown request ID", "danger")
return redirect(url_for("requests.index"))
RequestUser.query.filter(
(RequestUser.user_id == current_user.id)
& (RequestUser.item_id == request.item_id)
).update({RequestUser.updated: False})
current_app.db.session.commit()
jf_item = None
arr = request.arr_item
id_map = c.jellyfin.id_map()
for key_id in ["tmdbId", "imdbId", "tvdbId"]:
if key_id in arr:
key = (key_id.lower()[:-2], str(arr[key_id]))
jf_item = id_map.get(key, None)
if jf_item:
break
return render_template(
"requests/details.html", request=request, jellyfin_id=jf_item
)
@requests_page.route("/", methods=["GET", "POST"])
@login_required
def index():
c = Client()
form = RequestForm()
user_id = current_user.get_id()
cfg = handle_config()
used_requests = defaultdict(int)
if current_user.is_admin:
requests = RequestItem.query
else:
requests = RequestItem.query.filter(
RequestItem.users.any(RequestUser.user_id == user_id)
)
for item in requests:
if item.approved is None:
used_requests[item.request_type] += 1
remaining = {}
remaining["movies"] = (
cfg["num_requests_per_user"]["movies"] - used_requests["radarr"]
)
remaining["shows"] = cfg["num_requests_per_user"]["shows"] - used_requests["sonarr"]
print("RQs:", used_requests, remaining)
if (
request.method == "POST"
and ("approve" in request.form)
or ("decline" in request.form)
):
approved = "approve" in request.form
declined = "decline" in request.form
if approved and declined:
flash("What the fuck?")
approved = False
declined = False
if approved or declined:
new_state = approved
print("Approved:", approved)
for item_id in request.form.getlist("selected[]"):
item = RequestItem.query.get(item_id)
if item.approved != new_state:
RequestUser.query.filter(RequestUser.item_id == item_id).update(
{RequestUser.updated: True}
)
item.approved = new_state
if new_state is True:
search_type = item.request_type
if hasattr(c, search_type):
api = getattr(c, search_type)
if hasattr(api, "add"):
result = api.add(json.loads(item.data))
print(result)
if item.request_type == "sonarr":
item.arr_id = result["seriesId"]
if item.request_type == "radarr":
item.arr_id = result["id"]
else:
flash("Invalid search type: {}".format(search_type), "danger")
current_app.db.session.merge(item)
current_app.db.session.commit()
return render_template(
"requests/index.html",
results=[],
form=form,
search_type=None,
requests=requests,
)
return redirect(url_for("requests.index"))
if request.method == "POST" and form.search.data is False:
request_type = request.form["search_type"]
for item in request.form.getlist("selected[]"):
data = str(base64.b64decode(item), "utf8")
item = json.loads(data)
item_id = "{type}/{titleSlug}/{year}".format(type=request_type, **item)
user_id = session["jf_user"].get_id()
request_entry = RequestItem.query.get(item_id)
if request_entry is None:
request_entry = RequestItem(
added_date=datetime.now(),
item_id=item_id,
users=[],
data=data,
request_type=request_type,
)
current_app.db.session.add(request_entry)
request_entry.users.append(
RequestUser(
user_id=user_id, item_id=item_id, user_name=current_user["Name"]
)
)
current_app.db.session.merge(request_entry)
current_app.db.session.commit()
return render_template(
"requests/index.html", results=[], form=form, requests=requests
)
if form and form.validate_on_submit():
c = Client()
query = form.query.data
search_type = form.search_type.data
if hasattr(c, search_type):
api = getattr(c, search_type)
if hasattr(api, "search"):
results = api.search(query)
return render_template(
"requests/index.html",
results=results,
form=form,
search_type=search_type,
)
flash("Invalid search type: {}".format(search_type), "danger")
return render_template(
"requests/index.html",
results=[],
form=form,
search_type=None,
requests=requests,
)
"""
if form.validate_on_submit():
query = form.query.data
if not (form.torrents.data or form.movies.data or form.tv_shows.data):
form.torrents.data = True
form.movies.data = True
form.tv_shows.data = True
if form.torrents.data:
results["torrents"] = c.jackett.search(
query, form.indexer.data or form.indexer.choices
)
if form.movies.data:
results["movies"] = c.radarr.search(query)
if form.tv_shows.data:
results["tv_shows"] = c.sonarr.search(query)
return render_template(
"search/index.html",
# form=form,
search_term=query,
results=results,
client=c,
group_by_tracker=form.group_by_tracker.data,
)
"""

62
views/search/__init__.py Normal file
View file

@ -0,0 +1,62 @@
from urllib.parse import unquote_plus
from flask import Blueprint, json, render_template, request
from api import Client
from forms import SearchForm
from utils import admin_required
search_page = Blueprint("search", __name__, url_prefix="/search")
@search_page.route("/details", methods=["GET", "POST"])
@admin_required
def details():
data = {
"info": json.loads(unquote_plus(request.form["data"])),
"type": request.form["type"],
}
return render_template("search/details.html", **data)
@search_page.route("/", methods=["GET", "POST"])
@admin_required
def index():
c = Client()
results = {}
params = request.args
form = SearchForm()
form.indexer.choices = c.jackett.indexers()
if form.validate_on_submit():
query = form.query.data
if not (form.torrents.data or form.movies.data or form.tv_shows.data):
form.torrents.data = True
form.movies.data = True
form.tv_shows.data = True
if form.torrents.data:
results["torrents"] = c.jackett.search(
query, form.indexer.data or form.indexer.choices
)
if form.movies.data:
results["movies"] = c.radarr.search(query)
if form.tv_shows.data:
results["tv_shows"] = c.sonarr.search(query)
return render_template(
"search/index.html",
# form=form,
search_term=query,
results=results,
client=c,
group_by_tracker=form.group_by_tracker.data,
)
for name, field in form._fields.items():
field.default = params.get(name)
form.process()
return render_template(
"search/index.html",
form=form,
results={},
group_by_tracker=False,
sort_by="Gain",
)

24
views/sonarr/__init__.py Normal file
View file

@ -0,0 +1,24 @@
from flask import Blueprint, render_template
from api import Client
from utils import admin_required
sonarr_page = Blueprint("sonarr", __name__, url_prefix="/sonarr")
@sonarr_page.route("/")
@admin_required
def index():
c = Client()
series = c.sonarr.series()
status = c.sonarr.status()
history = c.sonarr.history()
return render_template(
"sonarr/index.html", series=series, status=status, history=history
)
@sonarr_page.route("/<show_id>")
@admin_required
def details(show_id):
return render_template("sonarr/details.html")

View file

@ -0,0 +1,10 @@
from flask import Blueprint, render_template
from utils import admin_required
transcode_page = Blueprint("transcode", __name__, url_prefix="/transcode")
@transcode_page.route("/", methods=["GET", "POST"])
@admin_required
def index():
return render_template("transcode/profiles.html")