SalexAI commited on
Commit
5d85607
·
verified ·
1 Parent(s): 1e19ce0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -1
app.py CHANGED
@@ -443,4 +443,185 @@ async def get_video_feed(div: Optional[int] = Query(None, description="School Di
443
  filtered_videos = []
444
  for v in videos:
445
  allowed = v.get("allowed_divs", [])
446
- if not allowed or
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  filtered_videos = []
444
  for v in videos:
445
  allowed = v.get("allowed_divs", [])
446
+ if not allowed or div in allowed:
447
+ filtered_videos.append(v)
448
+
449
+ return {"videos": filtered_videos}
450
+
451
+ # ==================================================
452
+ # ADMIN: LOGIN
453
+ # ==================================================
454
+ @app.get("/admin/login", response_class=HTMLResponse)
455
+ async def admin_login_page():
456
+ if not admin_enabled():
457
+ return HTMLResponse("Admin disabled (ADMIN_KEY missing)", status_code=503)
458
+ return """
459
+ <html><body style="background:#111;color:#fff;display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;">
460
+ <form method="post" style="padding:20px;border:1px solid #333;border-radius:10px;background:#1a1a1a;">
461
+ <h2>Admin</h2><input type="password" name="key" placeholder="Key" style="padding:10px;width:100%;margin-bottom:10px;">
462
+ <button style="padding:10px;width:100%;cursor:pointer;">Login</button>
463
+ </form></body></html>
464
+ """
465
+
466
+ @app.post("/admin/login")
467
+ async def admin_login(key: str = Form(...)):
468
+ if not admin_enabled() or not secure_equals(key.strip(), str(ADMIN_KEY).strip()):
469
+ return HTMLResponse("Unauthorized", status_code=401)
470
+ session = secrets.token_hex(16)
471
+ ADMIN_SESSIONS.add(session)
472
+ resp = RedirectResponse("/admin", status_code=302)
473
+ resp.set_cookie(ADMIN_COOKIE, session, httponly=True)
474
+ return resp
475
+
476
+ @app.get("/admin/logout")
477
+ async def admin_logout(req: Request):
478
+ if (s := req.cookies.get(ADMIN_COOKIE)) in ADMIN_SESSIONS: ADMIN_SESSIONS.remove(s)
479
+ return RedirectResponse("/admin/login")
480
+
481
+ # ==================================================
482
+ # ADMIN DASHBOARD
483
+ # ==================================================
484
+ def esc(v) -> str: return html_escape_lib.escape("" if v is None else str(v), quote=True)
485
+
486
+ ADMIN_TEMPLATE = """
487
+ <html>
488
+ <head>
489
+ <title>Admin</title>
490
+ <style>
491
+ body{font-family:sans-serif;background:#111;color:#ddd;padding:20px;}
492
+ table{width:100%;border-collapse:collapse;margin-top:20px;}
493
+ th,td{border-bottom:1px solid #333;padding:8px;text-align:left;}
494
+ input{background:#222;border:1px solid #444;color:#fff;padding:5px;border-radius:4px;width:100%;}
495
+ button{background:#444;color:#fff;border:none;padding:5px 10px;cursor:pointer;border-radius:4px;}
496
+ .pill{padding:4px 8px;background:#004400;border-radius:10px;font-size:0.8em;}
497
+ </style>
498
+ </head>
499
+ <body>
500
+ <h1>Admin Panel <a href="/admin/logout" style="font-size:0.5em;color:#888;">Logout</a></h1>
501
+
502
+ <h3>Albums</h3>
503
+ <table>
504
+ <tr><th>Token</th><th>Publisher</th><th>Category</th><th>Action</th></tr>
505
+ __ALBUM_ROWS__
506
+ <tr>
507
+ <td><input id="n_t" placeholder="Token"></td>
508
+ <td><input id="n_p" placeholder="Publisher"></td>
509
+ <td><input id="n_c" placeholder="Category"></td>
510
+ <td><button onclick="addAlbum()">Add</button></td>
511
+ </tr>
512
+ </table>
513
+
514
+ <h3>Videos</h3>
515
+ <p style="font-size:0.8em;color:#888;">Allowed Divs: Comma separated (e.g. <code>1,5,25</code>). Leave empty for ALL.</p>
516
+ <table>
517
+ <tr><th>ID</th><th>Name</th><th>Divs (1-25)</th><th>Category</th><th>Publisher</th><th>Action</th></tr>
518
+ __VIDEO_ROWS__
519
+ </table>
520
+
521
+ <script>
522
+ async function api(ep, data) {
523
+ await fetch(ep, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)});
524
+ location.reload();
525
+ }
526
+ document.querySelectorAll('input[data-id]').forEach(i => {
527
+ i.addEventListener('change', (e) => api('/admin/update', {
528
+ id: e.target.dataset.id,
529
+ field: e.target.dataset.field,
530
+ value: e.target.value
531
+ }));
532
+ });
533
+ function addAlbum(){
534
+ api('/admin/albums/add', {
535
+ token: document.getElementById('n_t').value,
536
+ publisher: document.getElementById('n_p').value,
537
+ category: document.getElementById('n_c').value
538
+ });
539
+ }
540
+ </script>
541
+ </body>
542
+ </html>
543
+ """
544
+
545
+ @app.get("/admin", response_class=HTMLResponse)
546
+ async def admin_dash(req: Request):
547
+ if not is_admin(req): return RedirectResponse("/admin/login")
548
+
549
+ async with DATA_LOCK:
550
+ videos = INDEX_CACHE.get("videos", [])
551
+
552
+ v_rows = ""
553
+ for v in videos:
554
+ divs = ",".join(map(str, v.get("allowed_divs", [])))
555
+ v_rows += f"""<tr>
556
+ <td>{esc(v['id'])[:8]}...</td>
557
+ <td><input data-id="{v['id']}" data-field="name" value="{esc(v.get('name'))}"></td>
558
+ <td><input data-id="{v['id']}" data-field="allowed_divs" value="{esc(divs)}" placeholder="All"></td>
559
+ <td><input data-id="{v['id']}" data-field="category" value="{esc(v.get('category'))}"></td>
560
+ <td><input data-id="{v['id']}" data-field="publisher" value="{esc(v.get('publisher'))}"></td>
561
+ <td><button onclick="api('/admin/videos/delete', {{id:'{v['id']}'}})" style="background:#500;">Del</button></td>
562
+ </tr>"""
563
+
564
+ a_rows = ""
565
+ for t, p in ALBUM_PUBLISHERS.items():
566
+ c = ALBUM_CATEGORIES.get(t, "")
567
+ a_rows += f"<tr><td>{t}</td><td>{p}</td><td>{c}</td><td>-</td></tr>"
568
+
569
+ return ADMIN_TEMPLATE.replace("__VIDEO_ROWS__", v_rows).replace("__ALBUM_ROWS__", a_rows)
570
+
571
+ # ==================================================
572
+ # ADMIN ACTIONS
573
+ # ==================================================
574
+ @app.post("/admin/update")
575
+ async def admin_update(req: Request, payload: dict):
576
+ if not is_admin(req): return JSONResponse({}, 403)
577
+
578
+ vid_id = payload.get("id")
579
+ field = payload.get("field")
580
+ value = payload.get("value")
581
+
582
+ async with DATA_LOCK:
583
+ for v in INDEX_CACHE["videos"]:
584
+ if v["id"] == vid_id:
585
+ if field == "allowed_divs":
586
+ try:
587
+ if not value.strip():
588
+ v[field] = []
589
+ else:
590
+ nums = [int(x.strip()) for x in value.split(",") if x.strip().isdigit()]
591
+ v[field] = [n for n in nums if n in VALID_DIVISIONS]
592
+ except:
593
+ pass
594
+ else:
595
+ v[field] = value
596
+
597
+ await save_index()
598
+ return {"ok": True}
599
+ return {"error": "not found"}
600
+
601
+ @app.post("/admin/videos/delete")
602
+ async def admin_delete(req: Request, payload: dict):
603
+ if not is_admin(req): return JSONResponse({}, 403)
604
+ vid_id = payload.get("id")
605
+
606
+ async with DATA_LOCK:
607
+ original_len = len(INDEX_CACHE["videos"])
608
+ INDEX_CACHE["videos"] = [v for v in INDEX_CACHE["videos"] if v["id"] != vid_id]
609
+
610
+ if len(INDEX_CACHE["videos"]) < original_len:
611
+ await save_index()
612
+
613
+ return {"ok": True}
614
+
615
+ @app.post("/admin/albums/add")
616
+ async def admin_album_add(req: Request, payload: dict):
617
+ if not is_admin(req): return JSONResponse({}, 403)
618
+ t = payload.get("token")
619
+ if t:
620
+ async with DATA_LOCK:
621
+ CONFIG_CACHE["album_publishers"][t] = payload.get("publisher", "Unknown")
622
+ CONFIG_CACHE["album_categories"][t] = payload.get("category", "Uncategorized")
623
+ global ALBUM_PUBLISHERS, ALBUM_CATEGORIES
624
+ ALBUM_PUBLISHERS = CONFIG_CACHE["album_publishers"]
625
+ ALBUM_CATEGORIES = CONFIG_CACHE["album_categories"]
626
+ await save_config()
627
+ return {"ok": True}