Compare commits
2 commits
86ec770ba4
...
a17f3383e3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17f3383e3 | ||
|
|
52048ad8f7 |
4 changed files with 151 additions and 16 deletions
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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,10 +7,13 @@ import urllib.parse
|
|||
from typing import Optional, List, Dict, Any
|
||||
|
||||
import requests
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
|
@ -78,12 +81,67 @@ settings = Settings()
|
|||
class SettingsUpdate(BaseModel):
|
||||
stashdb_url: str
|
||||
stashdb_api_key: str
|
||||
whisparr_url: str
|
||||
whisparr_api_key: str
|
||||
jellyfin_url: str
|
||||
whisparr_root_folder: 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
|
||||
class StashDBClient:
|
||||
def __init__(self, url: str, api_key: str):
|
||||
|
|
@ -626,9 +684,6 @@ def index_route(
|
|||
def update_settings(update: SettingsUpdate):
|
||||
settings.stashdb_url = update.stashdb_url
|
||||
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_quality_profile_id = update.whisparr_quality_profile_id or ""
|
||||
|
||||
|
|
|
|||
|
|
@ -718,14 +718,6 @@
|
|||
|
||||
<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>
|
||||
<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="flex flex-col gap-2">
|
||||
<label class="text-2xs font-semibold text-slate-600 uppercase tracking-widest">Root Folder</label>
|
||||
|
|
|
|||
59
templates/login.html
Normal file
59
templates/login.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!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