Upload
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import requests
|
||||
from flask import (
|
||||
Blueprint, current_app, flash, jsonify, redirect, render_template,
|
||||
request, session, url_for
|
||||
)
|
||||
|
||||
from .models import db, EventLog, Stream, StreamSession, User
|
||||
from .utils import ffprobe_stream, generate_token, hls_url, rtmp_url
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
|
||||
def current_user():
|
||||
uid = session.get('user_id')
|
||||
if not uid:
|
||||
return None
|
||||
return db.session.get(User, uid)
|
||||
|
||||
|
||||
def login_required(role=None):
|
||||
def deco(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
user = current_user()
|
||||
if not user or not user.active:
|
||||
return redirect(url_for('main.login'))
|
||||
if role and user.role != role and user.role != 'admin':
|
||||
flash('Ingen adgang til denne side.', 'error')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
return deco
|
||||
|
||||
|
||||
def log_event(stream_id, message, level='info'):
|
||||
db.session.add(EventLog(stream_id=stream_id, message=message, level=level))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def get_stats(stream: Stream) -> dict:
|
||||
url = hls_url(current_app.config['MEDIA_BASE_URL'], stream.slug)
|
||||
return ffprobe_stream(url) if stream.is_live else {}
|
||||
|
||||
|
||||
@bp.app_context_processor
|
||||
def inject_globals():
|
||||
return {'current_user': current_user()}
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
def index():
|
||||
if current_user():
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username', '').strip()
|
||||
password = request.form.get('password', '')
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.active and user.check_password(password):
|
||||
session['user_id'] = user.id
|
||||
flash('Du er nu logget ind.', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
flash('Forkert brugernavn eller kodeord.', 'error')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@bp.get('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
|
||||
@bp.get('/dashboard')
|
||||
@login_required()
|
||||
def dashboard():
|
||||
streams = Stream.query.order_by(Stream.name.asc()).all()
|
||||
return render_template('dashboard.html', streams=streams)
|
||||
|
||||
|
||||
@bp.route('/streams/new', methods=['GET', 'POST'])
|
||||
@login_required('admin')
|
||||
def stream_new():
|
||||
if request.method == 'POST':
|
||||
name = request.form.get('name', '').strip()
|
||||
slug = request.form.get('slug', '').strip().lower().replace(' ', '-')
|
||||
chat_type = request.form.get('chat_type', 'none')
|
||||
chat_url = request.form.get('chat_url', '').strip()
|
||||
notes = request.form.get('notes', '').strip()
|
||||
if not name or not slug:
|
||||
flash('Navn og slug er påkrævet.', 'error')
|
||||
return render_template('stream_form.html', stream=None)
|
||||
if Stream.query.filter_by(slug=slug).first():
|
||||
flash('Slug findes allerede.', 'error')
|
||||
return render_template('stream_form.html', stream=None)
|
||||
s = Stream(name=name, slug=slug, token=generate_token(), chat_type=chat_type, chat_url=chat_url, notes=notes)
|
||||
db.session.add(s)
|
||||
db.session.commit()
|
||||
log_event(s.id, 'Stream oprettet')
|
||||
flash('Stream oprettet.', 'success')
|
||||
return redirect(url_for('main.stream_detail', stream_id=s.id))
|
||||
return render_template('stream_form.html', stream=None)
|
||||
|
||||
|
||||
@bp.route('/streams/<int:stream_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required('admin')
|
||||
def stream_edit(stream_id):
|
||||
s = db.session.get(Stream, stream_id)
|
||||
if not s:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
if request.method == 'POST':
|
||||
s.name = request.form.get('name', s.name).strip()
|
||||
s.slug = request.form.get('slug', s.slug).strip().lower().replace(' ', '-')
|
||||
s.chat_type = request.form.get('chat_type', s.chat_type)
|
||||
s.chat_url = request.form.get('chat_url', s.chat_url).strip()
|
||||
s.notes = request.form.get('notes', s.notes).strip()
|
||||
s.active = bool(request.form.get('active'))
|
||||
db.session.commit()
|
||||
log_event(s.id, 'Stream opdateret')
|
||||
flash('Stream gemt.', 'success')
|
||||
return redirect(url_for('main.stream_detail', stream_id=s.id))
|
||||
return render_template('stream_form.html', stream=s)
|
||||
|
||||
|
||||
@bp.post('/streams/<int:stream_id>/rotate-token')
|
||||
@login_required('admin')
|
||||
def rotate_token(stream_id):
|
||||
s = db.session.get(Stream, stream_id)
|
||||
s.token = generate_token()
|
||||
db.session.commit()
|
||||
log_event(s.id, 'Token roteret', 'warning')
|
||||
flash('Token er fornyet.', 'success')
|
||||
return redirect(url_for('main.stream_detail', stream_id=stream_id))
|
||||
|
||||
|
||||
@bp.post('/streams/<int:stream_id>/toggle')
|
||||
@login_required('admin')
|
||||
def toggle_stream(stream_id):
|
||||
s = db.session.get(Stream, stream_id)
|
||||
s.active = not s.active
|
||||
db.session.commit()
|
||||
log_event(s.id, f"Stream {'aktiveret' if s.active else 'deaktiveret'}")
|
||||
flash('Streamstatus ændret.', 'success')
|
||||
return redirect(url_for('main.stream_detail', stream_id=stream_id))
|
||||
|
||||
|
||||
@bp.get('/streams/<int:stream_id>')
|
||||
@login_required()
|
||||
def stream_detail(stream_id):
|
||||
s = db.session.get(Stream, stream_id)
|
||||
if not s:
|
||||
return redirect(url_for('main.dashboard'))
|
||||
stats = get_stats(s)
|
||||
active_session = StreamSession.query.filter_by(stream_id=s.id, status='live').order_by(StreamSession.started_at.desc()).first()
|
||||
logs = EventLog.query.filter_by(stream_id=s.id).order_by(EventLog.created_at.desc()).limit(50).all()
|
||||
public_hls = hls_url(current_app.config['MEDIA_PUBLIC_HLS_BASE'], s.slug)
|
||||
ingest = rtmp_url(current_app.config['PUBLIC_BASE_URL'], s.slug, s.token)
|
||||
return render_template('stream_detail.html', stream=s, stats=stats, active_session=active_session, logs=logs, public_hls=public_hls, ingest=ingest)
|
||||
|
||||
|
||||
@bp.get('/streamer/<slug>')
|
||||
@login_required()
|
||||
def streamer_panel(slug):
|
||||
s = Stream.query.filter_by(slug=slug).first_or_404()
|
||||
stats = get_stats(s)
|
||||
public_hls = hls_url(current_app.config['MEDIA_PUBLIC_HLS_BASE'], s.slug)
|
||||
ingest = rtmp_url(current_app.config['PUBLIC_BASE_URL'], s.slug, s.token)
|
||||
return render_template('streamer_panel.html', stream=s, stats=stats, public_hls=public_hls, ingest=ingest)
|
||||
|
||||
|
||||
@bp.get('/api/streams/<int:stream_id>/status')
|
||||
@login_required()
|
||||
def stream_status(stream_id):
|
||||
s = db.session.get(Stream, stream_id)
|
||||
stats = get_stats(s)
|
||||
payload = {
|
||||
'id': s.id,
|
||||
'name': s.name,
|
||||
'slug': s.slug,
|
||||
'active': s.active,
|
||||
'is_live': s.is_live,
|
||||
'stats': stats,
|
||||
'chat_url': s.chat_url,
|
||||
}
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
@bp.post('/api/mediamtx/auth')
|
||||
def mediamtx_auth():
|
||||
data = request.get_json(silent=True) or {}
|
||||
action = data.get('action', '')
|
||||
path = data.get('path', '')
|
||||
token = data.get('token', '')
|
||||
stream = Stream.query.filter_by(slug=path).first()
|
||||
|
||||
# API/metrics m.m. er ekskluderet i mediamtx.yml og rammer normalt ikke her.
|
||||
if action in {'api', 'metrics', 'pprof'}:
|
||||
return ('ok', 200)
|
||||
|
||||
if action == 'publish':
|
||||
if not stream or not stream.active:
|
||||
return ('denied', 401)
|
||||
if token != stream.token:
|
||||
log_event(stream.id, f'Publish auth failed for path {path}', 'warning')
|
||||
return ('denied', 401)
|
||||
return ('ok', 200)
|
||||
|
||||
if action in {'read', 'playback'}:
|
||||
# I v1 er læsning åben, så preview virker uden ekstra token.
|
||||
if not stream:
|
||||
return ('denied', 401)
|
||||
return ('ok', 200)
|
||||
|
||||
return ('denied', 401)
|
||||
|
||||
|
||||
@bp.post('/api/mediamtx/hook/ready')
|
||||
def mediamtx_hook_ready():
|
||||
path = request.form.get('path', '')
|
||||
source_type = request.form.get('source_type', '')
|
||||
source_id = request.form.get('source_id', '')
|
||||
query = request.form.get('query', '')
|
||||
stream = Stream.query.filter_by(slug=path).first()
|
||||
if not stream:
|
||||
return ('missing', 404)
|
||||
|
||||
stream.is_live = True
|
||||
stream.last_ready_at = datetime.utcnow()
|
||||
sess = StreamSession.query.filter_by(stream_id=stream.id, status='live').order_by(StreamSession.started_at.desc()).first()
|
||||
if not sess:
|
||||
sess = StreamSession(stream_id=stream.id, status='live', source_type=source_type, source_id=source_id)
|
||||
db.session.add(sess)
|
||||
else:
|
||||
sess.status = 'live'
|
||||
sess.source_type = source_type
|
||||
sess.source_id = source_id
|
||||
|
||||
# Forsøg at hente metadata fra HLS-preview kort efter streamen er live.
|
||||
stats = get_stats(stream)
|
||||
for key, value in stats.items():
|
||||
setattr(sess, key, value)
|
||||
|
||||
db.session.commit()
|
||||
log_event(stream.id, f'Stream ready ({source_type})')
|
||||
return ('ok', 200)
|
||||
|
||||
|
||||
@bp.post('/api/mediamtx/hook/not-ready')
|
||||
def mediamtx_hook_not_ready():
|
||||
path = request.form.get('path', '')
|
||||
stream = Stream.query.filter_by(slug=path).first()
|
||||
if not stream:
|
||||
return ('missing', 404)
|
||||
stream.is_live = False
|
||||
stream.last_not_ready_at = datetime.utcnow()
|
||||
sess = StreamSession.query.filter_by(stream_id=stream.id, status='live').order_by(StreamSession.started_at.desc()).first()
|
||||
if sess:
|
||||
sess.status = 'offline'
|
||||
sess.ended_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
log_event(stream.id, 'Stream stopped', 'warning')
|
||||
return ('ok', 200)
|
||||
|
||||
|
||||
@bp.get('/healthz')
|
||||
def healthz():
|
||||
return jsonify({'ok': True})
|
||||
Reference in New Issue
Block a user