253 lines
7.1 KiB
Python
253 lines
7.1 KiB
Python
#!/bin/env python3
|
|
import os
|
|
import re
|
|
import secrets
|
|
import xml.dom.minidom
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
|
|
import requests
|
|
import toml
|
|
from flask import (
|
|
Flask,
|
|
make_response,
|
|
redirect,
|
|
render_template,
|
|
request,
|
|
url_for,
|
|
)
|
|
|
|
app = Flask(__name__)
|
|
|
|
release_groups = toml.load("/etc/nyaa-tools/release-groups.toml")
|
|
batch_groups = toml.load("/etc/nyaa-tools/batch-groups.toml")
|
|
|
|
DEV_ENV = bool(os.environ.get("DEV_ENV", default=False))
|
|
if not DEV_ENV:
|
|
PASSWORD = os.environ["PASSWORD"]
|
|
|
|
|
|
active_sessions = {}
|
|
|
|
|
|
def is_token_valid(token):
|
|
return (
|
|
token and token in active_sessions and datetime.now() < active_sessions[token]
|
|
)
|
|
|
|
|
|
# Creates a token and adds it to the "database"
|
|
def create_valid_token():
|
|
token = secrets.token_urlsafe(16)
|
|
|
|
current_time = datetime.now()
|
|
expiration_time = current_time + timedelta(days=1)
|
|
|
|
active_sessions[token] = expiration_time
|
|
|
|
return token
|
|
|
|
|
|
def remove_token(token):
|
|
del active_sessions[token]
|
|
|
|
|
|
@app.before_request
|
|
def before_request():
|
|
if not DEV_ENV:
|
|
if request.path == "/login":
|
|
return login()
|
|
else:
|
|
if not request.path.startswith("/static"):
|
|
token = request.cookies.get("session_token")
|
|
if not is_token_valid(token):
|
|
return redirect(url_for("login"))
|
|
|
|
|
|
@dataclass
|
|
class SearchItem:
|
|
title: str
|
|
link: str
|
|
|
|
def search(query) -> list[SearchItem]:
|
|
nyaa_search = requests.get("https://nyaa.si/?page=rss&q={}".format(query)).text
|
|
items = xml.dom.minidom.parseString(nyaa_search).getElementsByTagName("item")
|
|
|
|
result = []
|
|
|
|
for item in items:
|
|
title: str = item.getElementsByTagName("title")[0].firstChild.data # pyright: ignore[reportOptionalMemberAccess,reportAttributeAccessIssue]
|
|
link: str = item.getElementsByTagName("link")[0].firstChild.data # pyright: ignore[reportOptionalMemberAccess,reportAttributeAccessIssue]
|
|
|
|
result.append(SearchItem(title, link))
|
|
return result
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route("/login", methods=["GET", "POST"])
|
|
def login():
|
|
if request.method == "POST":
|
|
password = request.form["password"]
|
|
|
|
if password == PASSWORD:
|
|
token = create_valid_token()
|
|
|
|
# Set token in a cookie
|
|
resp = make_response(redirect(url_for("index")))
|
|
resp.set_cookie("session_token", token, max_age=60 * 60 * 24)
|
|
|
|
return resp
|
|
else:
|
|
return "Invalid credentials. Try again."
|
|
|
|
return render_template("login.html")
|
|
|
|
|
|
@app.route("/logout")
|
|
def logout():
|
|
# Get the session token
|
|
token = request.cookies.get("session_token")
|
|
|
|
remove_token(token)
|
|
|
|
# Clear the session and cookie
|
|
resp = make_response(redirect(url_for("login")))
|
|
resp.set_cookie("session_token", "", expires=0) # Remove cookie
|
|
return resp
|
|
|
|
|
|
@dataclass
|
|
class SeriesInfo:
|
|
release_group: str
|
|
title: str
|
|
season: int | None
|
|
|
|
|
|
# Returns the search page if q is empty, else it shows results
|
|
@app.route("/series", methods=["GET", "POST"])
|
|
def series():
|
|
if request.method == "GET":
|
|
query = request.args.get("q", default="", type=str)
|
|
|
|
if query == "":
|
|
return render_template("series/search.html")
|
|
|
|
results = search(query)
|
|
parsed_results = []
|
|
for item in results:
|
|
title = item.title
|
|
rg = re.search("\\[([^ ]+)\\].+", title)
|
|
if rg is None or rg.group(1) not in release_groups.keys():
|
|
continue
|
|
info = re.search(release_groups[rg.group(1)], title)
|
|
# TODO: Do not append duplicated entries
|
|
if info is None:
|
|
continue
|
|
try:
|
|
season = int(info.group("season"))
|
|
except IndexError:
|
|
season = None
|
|
parsed_results.append(
|
|
SeriesInfo(
|
|
release_group=rg.group(1),
|
|
title=info.group("title"),
|
|
season=season
|
|
)
|
|
)
|
|
return render_template("series/results.html", results=parsed_results)
|
|
elif request.method == "POST":
|
|
release_group = request.form["release-group"]
|
|
title = request.form["title"]
|
|
season = request.form["season"]
|
|
if release_group and title and season:
|
|
try:
|
|
season = int(season)
|
|
except ValueError:
|
|
return "Invalid season", 400
|
|
|
|
series = toml.load("/etc/nyaa-tools/series.toml")
|
|
if "tracking" not in series.keys():
|
|
series["tracking"] = []
|
|
|
|
# TODO: Do not allow duplicates
|
|
entry = {"name": title, "release_group": release_group, "season": season}
|
|
series["tracking"].append(entry)
|
|
with open("/etc/nyaa-tools/series.toml", "w") as f:
|
|
toml.dump(series, f)
|
|
return "OK", 200
|
|
else:
|
|
return "Missing parameters", 400
|
|
|
|
|
|
@dataclass
|
|
class BatchInfo:
|
|
release_group: str
|
|
title: str
|
|
season: int | None
|
|
episodes: int | None
|
|
link: str
|
|
|
|
@app.route("/batch", methods=["GET", "POST"])
|
|
def batch():
|
|
if request.method == "GET":
|
|
query = request.args.get("q", default="", type=str)
|
|
|
|
if query == "":
|
|
return render_template("batch/search.html")
|
|
|
|
results = search(query)
|
|
parsed_results = []
|
|
for item in results:
|
|
title = item.title
|
|
rg = re.search("\\[([^ ]+)\\].+", title)
|
|
if rg is None or rg.group(1) not in batch_groups.keys():
|
|
continue
|
|
info = re.search(batch_groups[rg.group(1)], title)
|
|
# TODO: Do not append duplicated entries
|
|
if info is None:
|
|
continue
|
|
try:
|
|
season = int(info.group("season"))
|
|
except IndexError:
|
|
season = None
|
|
|
|
try:
|
|
episodes = int(info.group("last")) - int(info.group("first")) + 1
|
|
except IndexError:
|
|
#TODO: Get episodes from file list (requires lxml & cssselector)
|
|
episodes = None
|
|
|
|
parsed_results.append(
|
|
BatchInfo(
|
|
release_group=rg.group(1),
|
|
title=info.group("title"),
|
|
season=season,
|
|
episodes=episodes,
|
|
link=item.link
|
|
)
|
|
)
|
|
return render_template("batch/results.html", results=parsed_results)
|
|
elif request.method == "POST":
|
|
title = request.form["title"]
|
|
season = request.form["season"]
|
|
link = request.form["link"]
|
|
if title and season and link:
|
|
try:
|
|
season = int(season)
|
|
except ValueError:
|
|
return "Invalid season", 400
|
|
|
|
ret = os.system(" ".join(["nyaa-batch", "-t", "'{}'".format(title), "-s", str(season), "-l", "'{}'".format(link)]))
|
|
if ret == 0:
|
|
return "OK", 200
|
|
else:
|
|
return "Error", 500
|
|
else:
|
|
return "Missing parameters", 400
|
|
|
|
if __name__ == "__main__":
|
|
app.run(debug=DEV_ENV)
|