Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,28 +11,12 @@ import secrets
|
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
|
| 13 |
# ==================================================
|
| 14 |
-
#
|
| 15 |
-
# ==================================================
|
| 16 |
-
app = FastAPI()
|
| 17 |
-
logging.basicConfig(level=logging.INFO)
|
| 18 |
-
|
| 19 |
-
app.add_middleware(
|
| 20 |
-
CORSMiddleware,
|
| 21 |
-
allow_origins=["*"],
|
| 22 |
-
allow_methods=["*"],
|
| 23 |
-
allow_headers=["*"],
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
# ==================================================
|
| 27 |
-
# Admin config
|
| 28 |
# ==================================================
|
| 29 |
ADMIN_PIN = os.environ.get("ADMIN_PIN", "4286") # CHANGE THIS
|
| 30 |
ADMIN_COOKIE = "admin_session"
|
| 31 |
ADMIN_SESSIONS = set()
|
| 32 |
|
| 33 |
-
# ==================================================
|
| 34 |
-
# Storage
|
| 35 |
-
# ==================================================
|
| 36 |
DATA_ROOT = os.environ.get("PERSISTENT_DIR", "/data")
|
| 37 |
VIDEO_DIR = os.path.join(DATA_ROOT, "videos")
|
| 38 |
INDEX_FILE = os.path.join(DATA_ROOT, "index.json")
|
|
@@ -40,7 +24,20 @@ INDEX_FILE = os.path.join(DATA_ROOT, "index.json")
|
|
| 40 |
os.makedirs(VIDEO_DIR, exist_ok=True)
|
| 41 |
|
| 42 |
# ==================================================
|
| 43 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# ==================================================
|
| 45 |
ALBUM_PUBLISHERS = {
|
| 46 |
"B2c5n8hH4uWRoAW": "Alex Rose",
|
|
@@ -61,33 +58,28 @@ ALBUM_CATEGORIES = {
|
|
| 61 |
}
|
| 62 |
|
| 63 |
# ==================================================
|
| 64 |
-
#
|
| 65 |
-
# ==================================================
|
| 66 |
-
def is_admin(request: Request) -> bool:
|
| 67 |
-
return request.cookies.get(ADMIN_COOKIE) in ADMIN_SESSIONS
|
| 68 |
-
|
| 69 |
-
# ==================================================
|
| 70 |
-
# HTTP client
|
| 71 |
# ==================================================
|
| 72 |
async def get_client():
|
| 73 |
if not hasattr(app.state, "client"):
|
| 74 |
-
app.state.client = httpx.AsyncClient(timeout=
|
| 75 |
return app.state.client
|
| 76 |
|
| 77 |
# ==================================================
|
| 78 |
-
#
|
| 79 |
# ==================================================
|
| 80 |
-
|
| 81 |
-
|
|
|
|
| 82 |
|
| 83 |
-
def base62_to_int(
|
| 84 |
n = 0
|
| 85 |
-
for c in
|
| 86 |
-
n = n * 62 +
|
| 87 |
return n
|
| 88 |
|
| 89 |
# ==================================================
|
| 90 |
-
#
|
| 91 |
# ==================================================
|
| 92 |
ICLOUD_HEADERS = {
|
| 93 |
"Origin": "https://www.icloud.com",
|
|
@@ -96,12 +88,17 @@ ICLOUD_HEADERS = {
|
|
| 96 |
ICLOUD_PAYLOAD = '{"streamCtag":null}'
|
| 97 |
|
| 98 |
async def get_base_url(token: str) -> str:
|
| 99 |
-
n = base62_to_int(token[1:3]
|
| 100 |
return f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"
|
| 101 |
|
| 102 |
async def get_redirected_base_url(base_url: str, token: str) -> str:
|
| 103 |
client = await get_client()
|
| 104 |
-
r = await client.post(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
if r.status_code == 330:
|
| 106 |
host = r.json()["X-Apple-MMe-Host"]
|
| 107 |
return f"https://{host}/{token}/sharedstreams/"
|
|
@@ -109,19 +106,20 @@ async def get_redirected_base_url(base_url: str, token: str) -> str:
|
|
| 109 |
|
| 110 |
async def post_json(path: str, base_url: str, payload: str):
|
| 111 |
client = await get_client()
|
| 112 |
-
r = await client.post(base_url
|
| 113 |
r.raise_for_status()
|
| 114 |
return r.json()
|
| 115 |
|
| 116 |
-
async def get_metadata(base_url):
|
| 117 |
return (await post_json("webstream", base_url, ICLOUD_PAYLOAD)).get("photos", [])
|
| 118 |
|
| 119 |
-
async def get_asset_urls(base_url, guids):
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
|
| 123 |
# ==================================================
|
| 124 |
-
#
|
| 125 |
# ==================================================
|
| 126 |
def load_index():
|
| 127 |
if not os.path.exists(INDEX_FILE):
|
|
@@ -134,7 +132,7 @@ def save_index(data):
|
|
| 134 |
json.dump(data, f, indent=2)
|
| 135 |
|
| 136 |
# ==================================================
|
| 137 |
-
#
|
| 138 |
# ==================================================
|
| 139 |
async def download_file(url, path):
|
| 140 |
client = await get_client()
|
|
@@ -145,12 +143,12 @@ async def download_file(url, path):
|
|
| 145 |
f.write(chunk)
|
| 146 |
|
| 147 |
# ==================================================
|
| 148 |
-
#
|
| 149 |
# ==================================================
|
| 150 |
async def poll_album(token):
|
| 151 |
base = await get_redirected_base_url(await get_base_url(token), token)
|
| 152 |
-
|
| 153 |
-
guids = [p["photoGuid"] for p in
|
| 154 |
assets = await get_asset_urls(base, guids)
|
| 155 |
|
| 156 |
index = load_index()
|
|
@@ -159,15 +157,18 @@ async def poll_album(token):
|
|
| 159 |
album_dir = os.path.join(VIDEO_DIR, token)
|
| 160 |
os.makedirs(album_dir, exist_ok=True)
|
| 161 |
|
| 162 |
-
for p in
|
| 163 |
if p.get("mediaAssetType", "").lower() != "video":
|
| 164 |
continue
|
| 165 |
vid = p["photoGuid"]
|
| 166 |
if vid in known:
|
| 167 |
continue
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
| 171 |
if not best:
|
| 172 |
continue
|
| 173 |
|
|
@@ -176,23 +177,25 @@ async def poll_album(token):
|
|
| 176 |
continue
|
| 177 |
|
| 178 |
video_path = os.path.join(album_dir, f"{vid}.mp4")
|
| 179 |
-
await download_file(
|
|
|
|
|
|
|
| 180 |
|
| 181 |
thumb = ""
|
| 182 |
-
pf = derivatives.get("PosterFrame")
|
| 183 |
-
if pf
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
f"
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
|
| 191 |
index["videos"].append({
|
| 192 |
"id": vid,
|
| 193 |
"name": p.get("caption") or "Untitled",
|
| 194 |
"video_url": f"/media/{token}/{vid}.mp4",
|
| 195 |
-
"thumbnail": thumb,
|
| 196 |
"upload_date": p.get("creationDate") or datetime.now(timezone.utc).isoformat(),
|
| 197 |
"category": ALBUM_CATEGORIES.get(token, "Uncategorized"),
|
| 198 |
"publisher": ALBUM_PUBLISHERS.get(token, "Unknown"),
|
|
@@ -201,38 +204,41 @@ async def poll_album(token):
|
|
| 201 |
|
| 202 |
save_index(index)
|
| 203 |
|
| 204 |
-
# ==================================================
|
| 205 |
-
# Startup polling
|
| 206 |
-
# ==================================================
|
| 207 |
@app.on_event("startup")
|
| 208 |
-
async def
|
| 209 |
async def loop():
|
| 210 |
while True:
|
| 211 |
-
for
|
| 212 |
try:
|
| 213 |
-
await poll_album(
|
| 214 |
except Exception:
|
| 215 |
-
logging.exception("
|
| 216 |
await asyncio.sleep(60)
|
| 217 |
asyncio.create_task(loop())
|
| 218 |
|
| 219 |
# ==================================================
|
| 220 |
-
#
|
| 221 |
# ==================================================
|
| 222 |
@app.get("/feed/videos")
|
| 223 |
async def feed():
|
| 224 |
return load_index()
|
| 225 |
|
| 226 |
# ==================================================
|
| 227 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
# ==================================================
|
| 229 |
@app.get("/admin/login", response_class=HTMLResponse)
|
| 230 |
async def admin_login_page():
|
| 231 |
return """
|
| 232 |
-
<form method="post"
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
</form>
|
| 237 |
"""
|
| 238 |
|
|
@@ -240,18 +246,18 @@ async def admin_login_page():
|
|
| 240 |
async def admin_login(pin: str = Form(...)):
|
| 241 |
if pin != ADMIN_PIN:
|
| 242 |
return HTMLResponse("Wrong PIN", status_code=401)
|
| 243 |
-
|
| 244 |
-
ADMIN_SESSIONS.add(
|
| 245 |
r = RedirectResponse("/admin", 302)
|
| 246 |
-
r.set_cookie(ADMIN_COOKIE,
|
| 247 |
return r
|
| 248 |
|
| 249 |
# ==================================================
|
| 250 |
-
#
|
| 251 |
# ==================================================
|
| 252 |
@app.get("/admin", response_class=HTMLResponse)
|
| 253 |
-
async def admin(
|
| 254 |
-
if not is_admin(
|
| 255 |
return RedirectResponse("/admin/login", 302)
|
| 256 |
|
| 257 |
idx = load_index()
|
|
@@ -259,48 +265,52 @@ async def admin(request: Request):
|
|
| 259 |
for v in idx["videos"]:
|
| 260 |
rows += f"""
|
| 261 |
<tr>
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
</tr>
|
| 269 |
"""
|
| 270 |
|
| 271 |
return f"""
|
| 272 |
<table border=1>
|
| 273 |
-
|
| 274 |
-
|
| 275 |
</table>
|
| 276 |
<script>
|
| 277 |
document.querySelectorAll("input").forEach(i=>{
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
</script>
|
| 285 |
"""
|
| 286 |
|
| 287 |
# ==================================================
|
| 288 |
-
#
|
| 289 |
# ==================================================
|
| 290 |
@app.post("/admin/update")
|
| 291 |
-
async def admin_update(
|
| 292 |
-
if not is_admin(
|
| 293 |
-
return JSONResponse({"error":
|
| 294 |
|
| 295 |
idx = load_index()
|
| 296 |
for v in idx["videos"]:
|
| 297 |
if v["id"] == payload["id"]:
|
| 298 |
v[payload["field"]] = payload["value"]
|
| 299 |
save_index(idx)
|
| 300 |
-
return {"ok":
|
| 301 |
-
return {"error":
|
| 302 |
|
| 303 |
# ==================================================
|
| 304 |
-
#
|
| 305 |
# ==================================================
|
| 306 |
app.mount("/media", StaticFiles(directory=VIDEO_DIR), name="media")
|
|
|
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
|
| 13 |
# ==================================================
|
| 14 |
+
# CONFIG
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# ==================================================
|
| 16 |
ADMIN_PIN = os.environ.get("ADMIN_PIN", "4286") # CHANGE THIS
|
| 17 |
ADMIN_COOKIE = "admin_session"
|
| 18 |
ADMIN_SESSIONS = set()
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
DATA_ROOT = os.environ.get("PERSISTENT_DIR", "/data")
|
| 21 |
VIDEO_DIR = os.path.join(DATA_ROOT, "videos")
|
| 22 |
INDEX_FILE = os.path.join(DATA_ROOT, "index.json")
|
|
|
|
| 24 |
os.makedirs(VIDEO_DIR, exist_ok=True)
|
| 25 |
|
| 26 |
# ==================================================
|
| 27 |
+
# APP SETUP
|
| 28 |
+
# ==================================================
|
| 29 |
+
app = FastAPI()
|
| 30 |
+
logging.basicConfig(level=logging.INFO)
|
| 31 |
+
|
| 32 |
+
app.add_middleware(
|
| 33 |
+
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"],
|
| 35 |
+
allow_methods=["*"],
|
| 36 |
+
allow_headers=["*"],
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# ==================================================
|
| 40 |
+
# ALBUM METADATA
|
| 41 |
# ==================================================
|
| 42 |
ALBUM_PUBLISHERS = {
|
| 43 |
"B2c5n8hH4uWRoAW": "Alex Rose",
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
# ==================================================
|
| 61 |
+
# CLIENT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
# ==================================================
|
| 63 |
async def get_client():
|
| 64 |
if not hasattr(app.state, "client"):
|
| 65 |
+
app.state.client = httpx.AsyncClient(timeout=30)
|
| 66 |
return app.state.client
|
| 67 |
|
| 68 |
# ==================================================
|
| 69 |
+
# BASE62
|
| 70 |
# ==================================================
|
| 71 |
+
BASE_62_MAP = {c: i for i, c in enumerate(
|
| 72 |
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
| 73 |
+
)}
|
| 74 |
|
| 75 |
+
def base62_to_int(token: str) -> int:
|
| 76 |
n = 0
|
| 77 |
+
for c in token:
|
| 78 |
+
n = n * 62 + BASE_62_MAP[c]
|
| 79 |
return n
|
| 80 |
|
| 81 |
# ==================================================
|
| 82 |
+
# ICLOUD HELPERS
|
| 83 |
# ==================================================
|
| 84 |
ICLOUD_HEADERS = {
|
| 85 |
"Origin": "https://www.icloud.com",
|
|
|
|
| 88 |
ICLOUD_PAYLOAD = '{"streamCtag":null}'
|
| 89 |
|
| 90 |
async def get_base_url(token: str) -> str:
|
| 91 |
+
n = base62_to_int(token[1:3])
|
| 92 |
return f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"
|
| 93 |
|
| 94 |
async def get_redirected_base_url(base_url: str, token: str) -> str:
|
| 95 |
client = await get_client()
|
| 96 |
+
r = await client.post(
|
| 97 |
+
f"{base_url}webstream",
|
| 98 |
+
headers=ICLOUD_HEADERS,
|
| 99 |
+
data=ICLOUD_PAYLOAD,
|
| 100 |
+
follow_redirects=False,
|
| 101 |
+
)
|
| 102 |
if r.status_code == 330:
|
| 103 |
host = r.json()["X-Apple-MMe-Host"]
|
| 104 |
return f"https://{host}/{token}/sharedstreams/"
|
|
|
|
| 106 |
|
| 107 |
async def post_json(path: str, base_url: str, payload: str):
|
| 108 |
client = await get_client()
|
| 109 |
+
r = await client.post(f"{base_url}{path}", headers=ICLOUD_HEADERS, data=payload)
|
| 110 |
r.raise_for_status()
|
| 111 |
return r.json()
|
| 112 |
|
| 113 |
+
async def get_metadata(base_url: str):
|
| 114 |
return (await post_json("webstream", base_url, ICLOUD_PAYLOAD)).get("photos", [])
|
| 115 |
|
| 116 |
+
async def get_asset_urls(base_url: str, guids: list):
|
| 117 |
+
return (await post_json(
|
| 118 |
+
"webasseturls", base_url, json.dumps({"photoGuids": guids})
|
| 119 |
+
)).get("items", {})
|
| 120 |
|
| 121 |
# ==================================================
|
| 122 |
+
# INDEX HELPERS
|
| 123 |
# ==================================================
|
| 124 |
def load_index():
|
| 125 |
if not os.path.exists(INDEX_FILE):
|
|
|
|
| 132 |
json.dump(data, f, indent=2)
|
| 133 |
|
| 134 |
# ==================================================
|
| 135 |
+
# DOWNLOADER
|
| 136 |
# ==================================================
|
| 137 |
async def download_file(url, path):
|
| 138 |
client = await get_client()
|
|
|
|
| 143 |
f.write(chunk)
|
| 144 |
|
| 145 |
# ==================================================
|
| 146 |
+
# POLLING
|
| 147 |
# ==================================================
|
| 148 |
async def poll_album(token):
|
| 149 |
base = await get_redirected_base_url(await get_base_url(token), token)
|
| 150 |
+
metadata = await get_metadata(base)
|
| 151 |
+
guids = [p["photoGuid"] for p in metadata]
|
| 152 |
assets = await get_asset_urls(base, guids)
|
| 153 |
|
| 154 |
index = load_index()
|
|
|
|
| 157 |
album_dir = os.path.join(VIDEO_DIR, token)
|
| 158 |
os.makedirs(album_dir, exist_ok=True)
|
| 159 |
|
| 160 |
+
for p in metadata:
|
| 161 |
if p.get("mediaAssetType", "").lower() != "video":
|
| 162 |
continue
|
| 163 |
vid = p["photoGuid"]
|
| 164 |
if vid in known:
|
| 165 |
continue
|
| 166 |
|
| 167 |
+
best = max(
|
| 168 |
+
(d for k, d in p["derivatives"].items() if k.lower() != "posterframe"),
|
| 169 |
+
key=lambda d: int(d.get("fileSize") or 0),
|
| 170 |
+
default=None,
|
| 171 |
+
)
|
| 172 |
if not best:
|
| 173 |
continue
|
| 174 |
|
|
|
|
| 177 |
continue
|
| 178 |
|
| 179 |
video_path = os.path.join(album_dir, f"{vid}.mp4")
|
| 180 |
+
await download_file(
|
| 181 |
+
f"https://{asset['url_location']}{asset['url_path']}", video_path
|
| 182 |
+
)
|
| 183 |
|
| 184 |
thumb = ""
|
| 185 |
+
pf = p["derivatives"].get("PosterFrame")
|
| 186 |
+
if pf:
|
| 187 |
+
a = assets.get(pf["checksum"])
|
| 188 |
+
if a:
|
| 189 |
+
thumb = os.path.join(album_dir, f"{vid}.jpg")
|
| 190 |
+
await download_file(
|
| 191 |
+
f"https://{a['url_location']}{a['url_path']}", thumb
|
| 192 |
+
)
|
| 193 |
|
| 194 |
index["videos"].append({
|
| 195 |
"id": vid,
|
| 196 |
"name": p.get("caption") or "Untitled",
|
| 197 |
"video_url": f"/media/{token}/{vid}.mp4",
|
| 198 |
+
"thumbnail": f"/media/{token}/{vid}.jpg" if thumb else "",
|
| 199 |
"upload_date": p.get("creationDate") or datetime.now(timezone.utc).isoformat(),
|
| 200 |
"category": ALBUM_CATEGORIES.get(token, "Uncategorized"),
|
| 201 |
"publisher": ALBUM_PUBLISHERS.get(token, "Unknown"),
|
|
|
|
| 204 |
|
| 205 |
save_index(index)
|
| 206 |
|
|
|
|
|
|
|
|
|
|
| 207 |
@app.on_event("startup")
|
| 208 |
+
async def start_polling():
|
| 209 |
async def loop():
|
| 210 |
while True:
|
| 211 |
+
for token in ALBUM_PUBLISHERS:
|
| 212 |
try:
|
| 213 |
+
await poll_album(token)
|
| 214 |
except Exception:
|
| 215 |
+
logging.exception("Polling failed")
|
| 216 |
await asyncio.sleep(60)
|
| 217 |
asyncio.create_task(loop())
|
| 218 |
|
| 219 |
# ==================================================
|
| 220 |
+
# FEED
|
| 221 |
# ==================================================
|
| 222 |
@app.get("/feed/videos")
|
| 223 |
async def feed():
|
| 224 |
return load_index()
|
| 225 |
|
| 226 |
# ==================================================
|
| 227 |
+
# ADMIN AUTH
|
| 228 |
+
# ==================================================
|
| 229 |
+
def is_admin(req: Request):
|
| 230 |
+
return req.cookies.get(ADMIN_COOKIE) in ADMIN_SESSIONS
|
| 231 |
+
|
| 232 |
+
# ==================================================
|
| 233 |
+
# ADMIN LOGIN
|
| 234 |
# ==================================================
|
| 235 |
@app.get("/admin/login", response_class=HTMLResponse)
|
| 236 |
async def admin_login_page():
|
| 237 |
return """
|
| 238 |
+
<form method="post">
|
| 239 |
+
<h2>Admin Login</h2>
|
| 240 |
+
<input name="pin" type="password"/>
|
| 241 |
+
<button>Enter</button>
|
| 242 |
</form>
|
| 243 |
"""
|
| 244 |
|
|
|
|
| 246 |
async def admin_login(pin: str = Form(...)):
|
| 247 |
if pin != ADMIN_PIN:
|
| 248 |
return HTMLResponse("Wrong PIN", status_code=401)
|
| 249 |
+
session = secrets.token_hex(16)
|
| 250 |
+
ADMIN_SESSIONS.add(session)
|
| 251 |
r = RedirectResponse("/admin", 302)
|
| 252 |
+
r.set_cookie(ADMIN_COOKIE, session, httponly=True)
|
| 253 |
return r
|
| 254 |
|
| 255 |
# ==================================================
|
| 256 |
+
# ADMIN DASHBOARD
|
| 257 |
# ==================================================
|
| 258 |
@app.get("/admin", response_class=HTMLResponse)
|
| 259 |
+
async def admin(req: Request):
|
| 260 |
+
if not is_admin(req):
|
| 261 |
return RedirectResponse("/admin/login", 302)
|
| 262 |
|
| 263 |
idx = load_index()
|
|
|
|
| 265 |
for v in idx["videos"]:
|
| 266 |
rows += f"""
|
| 267 |
<tr>
|
| 268 |
+
<td>{v['id']}</td>
|
| 269 |
+
<td><input data-id="{v['id']}" data-field="name" value="{v['name']}"></td>
|
| 270 |
+
<td><input data-id="{v['id']}" data-field="upload_date" value="{v['upload_date']}"></td>
|
| 271 |
+
<td><input data-id="{v['id']}" data-field="category" value="{v['category']}"></td>
|
| 272 |
+
<td><input data-id="{v['id']}" data-field="publisher" value="{v['publisher']}"></td>
|
| 273 |
+
<td><input data-id="{v['id']}" data-field="thumbnail" value="{v['thumbnail']}"></td>
|
| 274 |
</tr>
|
| 275 |
"""
|
| 276 |
|
| 277 |
return f"""
|
| 278 |
<table border=1>
|
| 279 |
+
<tr><th>ID</th><th>Name</th><th>Date</th><th>Category</th><th>Publisher</th><th>Thumb</th></tr>
|
| 280 |
+
{rows}
|
| 281 |
</table>
|
| 282 |
<script>
|
| 283 |
document.querySelectorAll("input").forEach(i=>{
|
| 284 |
+
i.onchange=()=>fetch("/admin/update",{
|
| 285 |
+
method:"POST",
|
| 286 |
+
headers:{{"Content-Type":"application/json"}},
|
| 287 |
+
body:JSON.stringify({{
|
| 288 |
+
id:i.dataset.id,
|
| 289 |
+
field:i.dataset.field,
|
| 290 |
+
value:i.value
|
| 291 |
+
}})
|
| 292 |
+
})
|
| 293 |
+
})
|
| 294 |
</script>
|
| 295 |
"""
|
| 296 |
|
| 297 |
# ==================================================
|
| 298 |
+
# ADMIN UPDATE
|
| 299 |
# ==================================================
|
| 300 |
@app.post("/admin/update")
|
| 301 |
+
async def admin_update(req: Request, payload: dict):
|
| 302 |
+
if not is_admin(req):
|
| 303 |
+
return JSONResponse({"error":"unauthorized"},403)
|
| 304 |
|
| 305 |
idx = load_index()
|
| 306 |
for v in idx["videos"]:
|
| 307 |
if v["id"] == payload["id"]:
|
| 308 |
v[payload["field"]] = payload["value"]
|
| 309 |
save_index(idx)
|
| 310 |
+
return {"ok":True}
|
| 311 |
+
return {"error":"not found"}
|
| 312 |
|
| 313 |
# ==================================================
|
| 314 |
+
# MEDIA
|
| 315 |
# ==================================================
|
| 316 |
app.mount("/media", StaticFiles(directory=VIDEO_DIR), name="media")
|