Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7d046292 | ||
|
|
e16bbb2220 | ||
|
|
552f7ec910 | ||
|
|
b7f1c07a25 | ||
|
|
ca703ab355 | ||
|
|
6b7793b310 | ||
|
|
cf65779a92 | ||
|
|
3b7a6a472a | ||
|
|
80756669ad | ||
|
|
9a006e6e65 | ||
|
|
9bd615ecf9 | ||
|
|
890e40f3cf | ||
|
|
df4153a9dd | ||
|
|
38454618bb |
@@ -1,4 +0,0 @@
|
|||||||
.git
|
|
||||||
__pycache__
|
|
||||||
server.log
|
|
||||||
*.pyc
|
|
||||||
-18
@@ -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"]
|
|
||||||
@@ -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
|
|
||||||
@@ -200,9 +200,6 @@
|
|||||||
<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>
|
||||||
@@ -400,8 +397,6 @@
|
|||||||
}
|
}
|
||||||
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"},
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ 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])
|
||||||
@@ -151,45 +149,6 @@ 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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user