Compare commits
No commits in common. "a17f3383e345a61e63d10c28a31204e1003c13ce" and "86ec770ba45ee5b7b9b3b7432c273d3979636e18" have entirely different histories.
a17f3383e3
...
86ec770ba4
4 changed files with 16 additions and 151 deletions
|
|
@ -1,29 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
huntarr:
|
|
||||||
build: .
|
|
||||||
image: huntarr:latest
|
|
||||||
container_name: huntarr
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
# Change 8000 on the left to whatever port you want exposed on your host
|
|
||||||
- "8000:8000"
|
|
||||||
volumes:
|
|
||||||
# Mounts the local ./config folder to /config in the container to persist config.json
|
|
||||||
- ./config:/config
|
|
||||||
environment:
|
|
||||||
- TZ=America/New_York
|
|
||||||
- CONFIG_PATH=/config/config.json
|
|
||||||
# You can also pass these via environment variables if you prefer not using the JSON
|
|
||||||
# - STASHDB_URL=https://stashdb.org/graphql
|
|
||||||
# - STASHDB_API_KEY=your_key_here
|
|
||||||
# - WHISPARR_URL=http://whisparr:6969
|
|
||||||
# - WHISPARR_API_KEY=your_key_here
|
|
||||||
# - JELLYFIN_URL=http://jellyfin:8096
|
|
||||||
networks:
|
|
||||||
- mediaserver_default
|
|
||||||
|
|
||||||
networks:
|
|
||||||
mediaserver_default:
|
|
||||||
external: true
|
|
||||||
71
main.py
71
main.py
|
|
@ -7,13 +7,10 @@ import urllib.parse
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import FastAPI, Request, HTTPException, Depends, Form
|
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import base64
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
@ -81,67 +78,12 @@ settings = Settings()
|
||||||
class SettingsUpdate(BaseModel):
|
class SettingsUpdate(BaseModel):
|
||||||
stashdb_url: str
|
stashdb_url: str
|
||||||
stashdb_api_key: str
|
stashdb_api_key: str
|
||||||
|
whisparr_url: str
|
||||||
|
whisparr_api_key: str
|
||||||
|
jellyfin_url: str
|
||||||
whisparr_root_folder: Optional[str] = ""
|
whisparr_root_folder: Optional[str] = ""
|
||||||
whisparr_quality_profile_id: Optional[str] = ""
|
whisparr_quality_profile_id: Optional[str] = ""
|
||||||
|
|
||||||
def sign_data(data: str) -> str:
|
|
||||||
secret = (settings.whisparr_api_key or "default_secret").encode()
|
|
||||||
h = hmac.new(secret, data.encode(), hashlib.sha256).digest()
|
|
||||||
return f"{data}.{base64.urlsafe_b64encode(h).decode()}"
|
|
||||||
|
|
||||||
def verify_data(signed_data: str) -> bool:
|
|
||||||
try:
|
|
||||||
data, sig = signed_data.rsplit(".", 1)
|
|
||||||
expected_sig = base64.urlsafe_b64encode(hmac.new((settings.whisparr_api_key or "default_secret").encode(), data.encode(), hashlib.sha256).digest()).decode()
|
|
||||||
return hmac.compare_digest(sig, expected_sig)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def auth_middleware(request: Request, call_next):
|
|
||||||
path = request.url.path
|
|
||||||
if path in ["/login", "/logout"] or path.startswith("/static"):
|
|
||||||
return await call_next(request)
|
|
||||||
|
|
||||||
auth_cookie = request.cookies.get("huntarr_auth")
|
|
||||||
if not auth_cookie or not verify_data(auth_cookie):
|
|
||||||
if path.startswith("/api"):
|
|
||||||
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
||||||
return RedirectResponse("/login", status_code=303)
|
|
||||||
|
|
||||||
return await call_next(request)
|
|
||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
|
||||||
def login_get(request: Request):
|
|
||||||
return templates.TemplateResponse(request=request, name="login.html", context={})
|
|
||||||
|
|
||||||
@app.post("/login", response_class=HTMLResponse)
|
|
||||||
def login_post(request: Request, username: str = Form(...), password: str = Form(...)):
|
|
||||||
jf_url = settings.jellyfin_url.rstrip("/")
|
|
||||||
try:
|
|
||||||
res = requests.post(
|
|
||||||
f"{jf_url}/Users/AuthenticateByName",
|
|
||||||
json={"Username": username, "Pw": password},
|
|
||||||
headers={"X-Emby-Authorization": 'MediaBrowser Client="Huntarr", Device="Server", DeviceId="Huntarr", Version="1.0"'},
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
if res.status_code == 200:
|
|
||||||
data = res.json()
|
|
||||||
if data.get("AccessToken"):
|
|
||||||
response = RedirectResponse("/", status_code=303)
|
|
||||||
response.set_cookie("huntarr_auth", sign_data(username), httponly=True, max_age=86400*30)
|
|
||||||
return response
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Jellyfin auth error: {e}")
|
|
||||||
|
|
||||||
return templates.TemplateResponse(request=request, name="login.html", context={"error": "Invalid username or password or Jellyfin unreachable."})
|
|
||||||
|
|
||||||
@app.get("/logout")
|
|
||||||
def logout():
|
|
||||||
response = RedirectResponse("/login", status_code=303)
|
|
||||||
response.delete_cookie("huntarr_auth")
|
|
||||||
return response
|
|
||||||
|
|
||||||
# GraphQL / StashDB Client
|
# GraphQL / StashDB Client
|
||||||
class StashDBClient:
|
class StashDBClient:
|
||||||
def __init__(self, url: str, api_key: str):
|
def __init__(self, url: str, api_key: str):
|
||||||
|
|
@ -684,6 +626,9 @@ def index_route(
|
||||||
def update_settings(update: SettingsUpdate):
|
def update_settings(update: SettingsUpdate):
|
||||||
settings.stashdb_url = update.stashdb_url
|
settings.stashdb_url = update.stashdb_url
|
||||||
settings.stashdb_api_key = update.stashdb_api_key
|
settings.stashdb_api_key = update.stashdb_api_key
|
||||||
|
settings.whisparr_url = update.whisparr_url
|
||||||
|
settings.whisparr_api_key = update.whisparr_api_key
|
||||||
|
settings.jellyfin_url = update.jellyfin_url
|
||||||
settings.whisparr_root_folder = update.whisparr_root_folder or ""
|
settings.whisparr_root_folder = update.whisparr_root_folder or ""
|
||||||
settings.whisparr_quality_profile_id = update.whisparr_quality_profile_id or ""
|
settings.whisparr_quality_profile_id = update.whisparr_quality_profile_id or ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,14 @@
|
||||||
|
|
||||||
<div class="border-t border-slate-900 pt-4 flex flex-col gap-3">
|
<div class="border-t border-slate-900 pt-4 flex flex-col gap-3">
|
||||||
<span class="text-2xs font-bold text-slate-700 uppercase tracking-widest">Whisparr</span>
|
<span class="text-2xs font-bold text-slate-700 uppercase tracking-widest">Whisparr</span>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-2xs font-semibold text-slate-600 uppercase tracking-widest">Base URL</label>
|
||||||
|
<input type="url" name="whisparr_url" value="{{ config.whisparr_url }}" placeholder="http://localhost:6969" class="w-full px-3 py-2">
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-2xs font-semibold text-slate-600 uppercase tracking-widest">API Key</label>
|
||||||
|
<input type="password" name="whisparr_api_key" value="{{ config.whisparr_api_key }}" placeholder="API Key" class="w-full px-3 py-2">
|
||||||
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-2xs font-semibold text-slate-600 uppercase tracking-widest">Root Folder</label>
|
<label class="text-2xs font-semibold text-slate-600 uppercase tracking-widest">Root Folder</label>
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
||||||
<title>Huntarr – Login</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<style>
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
html { background: #030507; }
|
|
||||||
body {
|
|
||||||
background: #030507;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
background: #0d1117;
|
|
||||||
border: 1px solid #1a2035;
|
|
||||||
color: #e2e8f0;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
input:focus {
|
|
||||||
border-color: #3b82f6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="flex items-center justify-center min-h-screen p-4">
|
|
||||||
<div class="w-full max-w-sm flex flex-col gap-6">
|
|
||||||
<div class="text-center">
|
|
||||||
<h1 class="text-2xl font-black tracking-tighter text-slate-100">Huntarr</h1>
|
|
||||||
<p class="text-xs text-slate-500 mt-1">Sign in with Jellyfin</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div class="bg-red-500/10 border border-red-500/20 rounded p-3 text-center">
|
|
||||||
<p class="text-xs font-semibold text-red-400">{{ error }}</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="/login" method="POST" class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<label class="text-2xs font-bold text-slate-600 uppercase tracking-widest">Username</label>
|
|
||||||
<input type="text" name="username" required autofocus class="px-3 py-2.5 rounded-lg text-sm w-full">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
<label class="text-2xs font-bold text-slate-600 uppercase tracking-widest">Password</label>
|
|
||||||
<input type="password" name="password" required class="px-3 py-2.5 rounded-lg text-sm w-full">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="mt-2 w-full py-2.5 rounded-lg font-bold text-sm bg-slate-100 hover:bg-white text-slate-950 transition-colors">
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Add table
Reference in a new issue