Author SHA1 Message Date
Omarchy-Big-PC e2523060d1 Merge branch 'main' into tmp 2026-08-28 16:47:15 +01:00
Omarchy-Big-PC 8d194d5241 Highlight export: CSV/JSON download, same-name highlights disambiguated per day, sorted chronologically 2026-08-28 16:47:14 +01:00
Omarchy-Big-PC 6b7d046292 Merge branch 'main' into tmp-deployment/proxmox-lxc 2026-08-28 16:18:08 +01:00
Omarchy-Big-PC 43561d5012 Add /api/rescan endpoint, header rescan button, 30s auto-poll for new files 2026-08-28 16:18:07 +01:00
Omarchy-Big-PC e16bbb2220 Merge main: metadata-driven uploads, multi-file queue, collision suffixes 2026-08-28 16:04:56 +01:00
human insan 552f7ec910 Show one archive time in player 2026-08-27 23:28:55 +01:00
human insan b7f1c07a25 Simplify player controls 2026-08-27 23:24:44 +01:00
human insan ca703ab355 Align responsive player controls 2026-08-27 23:20:37 +01:00
human insan 6b7793b310 Improve player controls on small screens 2026-08-27 23:18:23 +01:00
human insan cf65779a92 Make timeline legend jump between events 2026-08-27 23:14:51 +01:00
human insan 3b7a6a472a Start clips from timeline color markers 2026-08-27 23:09:25 +01:00
human insan 80756669ad Jump from event markers to clip starts 2026-08-27 23:09:25 +01:00
human insan 9a006e6e65 Display recorder system highlights 2026-08-27 23:09:25 +01:00
human insan 9bd615ecf9 Show highlights on timeline 2026-08-27 23:09:15 +01:00
human insan 890e40f3cf Simplify highlight API for automation 2026-08-27 23:09:15 +01:00
human insan df4153a9dd Add highlight marking and AI API 2026-08-27 23:09:15 +01:00
human insan 38454618bb Add Proxmox LXC deployment branch 2026-08-27 22:36:29 +01:00
5 changed files with 68 additions and 34 deletions
-4
View File
@@ -1,4 +0,0 @@
.git
__pycache__
server.log
*.pyc
-18
View File
@@ -1,18 +0,0 @@
FROM python:3.13-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY server.py index.html README.md ./
ENV C41_SOURCE=/data/source \
C41_OUTPUT=/data/encoded \
C41_HOST=0.0.0.0 \
C41_PORT=8765
EXPOSE 8765
VOLUME ["/data/source", "/data/encoded"]
CMD ["python3", "server.py"]
-12
View File
@@ -1,12 +0,0 @@
services:
c41-player:
build: .
container_name: c41-player
environment:
C41_HOST: 0.0.0.0
ports:
- "8765:8765"
volumes:
- "/home/human/Videos/Yassine Salon/C41/media:/data/source:ro"
- "/home/human/Videos/Yassine Salon/C41/media-mp4:/data/encoded"
restart: unless-stopped
+22
View File
@@ -136,6 +136,8 @@
<div class="page"> <div class="page">
<div class="header"> <div class="header">
<h1>C41 Archive</h1> <h1>C41 Archive</h1>
<span class="spacer" style="flex:1"></span>
<button id="rescan-btn" title="Rescan the archive folder" style="background:var(--surface);color:var(--text);border:none;padding:6px 10px;border-radius:3px;font-size:14px;cursor:pointer;margin-right:8px">&#x21bb;</button>
<select id="date"></select> <select id="date"></select>
</div> </div>
@@ -198,6 +200,9 @@
<button id="mark-end">Mark end</button><span class="mark-time" id="highlight-end">--:--:--</span> <button id="mark-end">Mark end</button><span class="mark-time" id="highlight-end">--:--:--</span>
<input id="highlight-label" placeholder="Highlight name" maxlength="120"> <input id="highlight-label" placeholder="Highlight name" maxlength="120">
<button id="save-highlight">Add highlight</button> <button id="save-highlight">Add highlight</button>
<span style="flex:1"></span>
<button id="export-csv" title="Download all highlights as CSV">Export CSV</button>
<button id="export-json" title="Download all highlights as JSON">Export JSON</button>
</div> </div>
<div class="highlight-list" id="highlight-list"><span style="color:var(--dim);font-size:12px">No highlights yet.</span></div> <div class="highlight-list" id="highlight-list"><span style="color:var(--dim);font-size:12px">No highlights yet.</span></div>
</section> </section>
@@ -395,6 +400,8 @@
} }
document.querySelector("#mark-start").onclick = () => setMark("start"); document.querySelector("#mark-start").onclick = () => setMark("start");
document.querySelector("#mark-end").onclick = () => setMark("end"); document.querySelector("#mark-end").onclick = () => setMark("end");
document.querySelector("#export-csv").onclick = () => location.href = "/api/highlights/export?format=csv";
document.querySelector("#export-json").onclick = () => location.href = "/api/highlights/export?format=json";
document.querySelector("#save-highlight").onclick = async () => { document.querySelector("#save-highlight").onclick = async () => {
if (highlightStart === null || highlightEnd === null || highlightStart >= highlightEnd) return; if (highlightStart === null || highlightEnd === null || highlightStart >= highlightEnd) return;
await fetch("/api/highlights", {method: "POST", headers: {"Content-Type": "application/json"}, await fetch("/api/highlights", {method: "POST", headers: {"Content-Type": "application/json"},
@@ -707,6 +714,21 @@
dateSelect.addEventListener("change", () => loadDate(dateSelect.value)); dateSelect.addEventListener("change", () => loadDate(dateSelect.value));
addEventListener("resize", drawTimeline); addEventListener("resize", drawTimeline);
// rescan: button + silent auto-poll for new dates
const rescanBtn = document.querySelector("#rescan-btn");
async function rescan(notify) {
try {
const r = await fetch("/api/rescan", {method: "POST"});
const d = await r.json();
if (notify) setStatus(`${d.days} day${d.days !== 1 ? "s" : ""}, ${d.clips} clip${d.clips !== 1 ? "s" : ""} indexed`);
loadDate(dateSelect.value || "");
return d;
} catch { return null; }
}
rescanBtn.addEventListener("click", () => rescan(true));
setInterval(() => { if (!queue.some(e => e.state === "busy")) rescan(false); }, 30000);
loadDate("").catch(() => {}); loadDate("").catch(() => {});
updateEncodeStatus(); updateEncodeStatus();
</script> </script>
+46
View File
@@ -102,6 +102,8 @@ class PlayerHandler(BaseHTTPRequestHandler):
return self.send_json(self.server.encode_status) return self.send_json(self.server.encode_status)
if request.path == "/api/highlights": if request.path == "/api/highlights":
return self.send_json(self.server.highlights) return self.send_json(self.server.highlights)
if request.path == "/api/highlights/export":
return self.export_highlights()
if request.path == "/api/system-highlights": if request.path == "/api/system-highlights":
date = parse_qs(request.query).get("date", [""])[0] date = parse_qs(request.query).get("date", [""])[0]
return self.send_json([item for item in self.server.system_highlights if item["date"] == date]) return self.send_json([item for item in self.server.system_highlights if item["date"] == date])
@@ -111,6 +113,11 @@ class PlayerHandler(BaseHTTPRequestHandler):
def do_POST(self): def do_POST(self):
path = urlparse(self.path).path path = urlparse(self.path).path
if path == "/api/rescan":
self.server.recordings = scan_recordings(self.server.output_root)
self.server.system_highlights = scan_system_highlights(self.server.source_root)
count = sum(map(len, self.server.recordings.values()))
return self.send_json({"ok": True, "days": len(self.server.recordings), "clips": count})
if path == "/api/upload": if path == "/api/upload":
return self.handle_upload() return self.handle_upload()
if path == "/api/encode": if path == "/api/encode":
@@ -144,6 +151,45 @@ class PlayerHandler(BaseHTTPRequestHandler):
return self.send_json(highlight) return self.send_json(highlight)
self.send_error(404) self.send_error(404)
def export_highlights(self):
fmt = parse_qs(urlparse(self.path).query).get("format", ["csv"])[0].lower()
items = sorted(self.server.highlights, key=lambda h: (h["date"], h["start"], h["id"]))
seen = {}
rows = []
for item in items:
# same label may repeat across days; within one day duplicates get -2, -3...
key = (item["date"], item["label"])
seen[key] = seen.get(key, 0) + 1
name = item["label"] if seen[key] == 1 else f"{item['label']} ({seen[key]})"
def clock(sec):
return f"{sec // 3600:02d}:{sec % 3600 // 60:02d}:{sec % 60:02d}"
rows.append({
"date": f"{item['date'][:4]}-{item['date'][4:6]}-{item['date'][6:]}",
"name": name,
"start": clock(item["start"]),
"end": clock(item["end"]),
"duration_sec": item["end"] - item["start"],
"clip": f"{item['date']}/{item['start'] // 3600:02d}.{item['start'] % 3600 // 60:02d}.{item['start'] % 60:02d}",
})
if fmt == "json":
data = json.dumps(rows, indent=2).encode()
ctype = "application/json"
else:
import io
buf = io.StringIO()
writer = __import__("csv").writer(buf)
writer.writerow(["date", "name", "start", "end", "duration_sec", "clip"])
writer.writerows((r["date"], r["name"], r["start"], r["end"], r["duration_sec"], r["clip"]) for r in rows)
data = buf.getvalue().encode()
ctype = "text/csv"
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
self.send_response(200)
self.send_header("Content-Type", f"{ctype}; charset=utf-8")
self.send_header("Content-Disposition", f'attachment; filename="highlights-{stamp}.{fmt}"')
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def handle_upload(self): def handle_upload(self):
try: try:
length = int(self.headers.get("Content-Length", "0")) length = int(self.headers.get("Content-Length", "0"))