# HG changeset patch
# User drewp@bigasterisk.com
# Date 1681604583 25200
# Node ID 53d99454f394ce57caa4b24fa9b9361593a59c59
# Parent 4f337f9ab80e2987c21f3308b7b077e4bb4b1acb
support dropping url or file
diff -r 4f337f9ab80e -r 53d99454f394 Dockerfile
--- a/Dockerfile Sat Apr 15 16:12:26 2023 -0700
+++ b/Dockerfile Sat Apr 15 17:23:03 2023 -0700
@@ -29,4 +29,4 @@
COPY vite.config.ts serve-files.js ./
COPY src/ ./src/
-COPY video.py video_service.py ./
+COPY *.py ./
diff -r 4f337f9ab80e -r 53d99454f394 pyproject.toml
--- a/pyproject.toml Sat Apr 15 16:12:26 2023 -0700
+++ b/pyproject.toml Sat Apr 15 17:23:03 2023 -0700
@@ -17,6 +17,7 @@
"starlette>=0.26.1",
"uvicorn>=0.21.1",
"watchgod>=0.8.2",
+ "sse-starlette>=1.3.3",
]
requires-python = ">=3.10"
license = { text = "MIT" }
diff -r 4f337f9ab80e -r 53d99454f394 skaffold.yaml
--- a/skaffold.yaml Sat Apr 15 16:12:26 2023 -0700
+++ b/skaffold.yaml Sat Apr 15 17:23:03 2023 -0700
@@ -7,7 +7,7 @@
- image: bang5:5000/video_image
sync:
infer:
- - src/*
+ - src/**
- '*.py'
- 'vite.config.ts'
tagPolicy:
diff -r 4f337f9ab80e -r 53d99454f394 src/ingest/IngestDrop.ts
--- a/src/ingest/IngestDrop.ts Sat Apr 15 16:12:26 2023 -0700
+++ b/src/ingest/IngestDrop.ts Sat Apr 15 17:23:03 2023 -0700
@@ -3,14 +3,58 @@
@customElement("ingest-drop")
export class IngestDrop extends LitElement {
-
static styles = [
css`
+ #drop {
+ min-width: 10em;
+ min-height: 10em;
+ background: gray;
+ border: 5px gray dashed;
+ margin: 1% 10%;
+ }
+ #drop.dnd-hover {
+ background: yellow;
+ }
`,
];
render() {
- return html`
-
Drop video urls here
- `;
+ return html` Drop video url here
`;
+ }
+ dragover(ev: DragEvent) {
+ this.shadowRoot?.querySelector("#drop")?.classList.add("dnd-hover");
+ ev.preventDefault();
+ if (ev.dataTransfer) {
+ ev.dataTransfer.dropEffect = "copy";
+ }
+ }
+ leave() {
+ this.shadowRoot?.querySelector("#drop")?.classList.remove("dnd-hover");
+ }
+ drop(ev: DragEvent) {
+ ev.preventDefault();
+ this.leave();
+
+ if (!ev.dataTransfer) {
+ return;
+ }
+
+ for (let i = 0; i < ev.dataTransfer.files.length; i++) {
+ const f = ev.dataTransfer.files[i];
+ const name = f.name;
+ const stream = f.stream();
+ fetch("../api/ingest/videoUpload?name=" + encodeURIComponent(f.name), {
+ method: "POST",
+ body: stream,
+ duplex: "half",
+ });
+ }
+
+ const url = ev.dataTransfer.getData("text/plain");
+ if (url) {
+ fetch("../api/ingest/videoUrl", {
+ method: "POST",
+ body: url,
+ });
+ }
}
}
diff -r 4f337f9ab80e -r 53d99454f394 video.py
--- a/video.py Sat Apr 15 16:12:26 2023 -0700
+++ b/video.py Sat Apr 15 17:23:03 2023 -0700
@@ -1,72 +1,20 @@
-import asyncio
-from dataclasses import dataclass
+import logging
from pathlib import Path
-import hashlib
-import logging
-import re
-from typing import Iterable
from prometheus_client import Gauge
from starlette.applications import Starlette
from starlette.requests import Request
-from starlette.responses import HTMLResponse, JSONResponse
+from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import Route
from starlette_exporter import PrometheusMiddleware, handle_metrics
+from sse_starlette.sse import EventSourceResponse
-from video_service import VideoFile
+from video_file_store import VideoFileStore
+from video_ingest import VideoIngest
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger()
-def vf(p: Path, label: str):
- return VideoFile(p, './files/' + str(p.relative_to('/data')), label)
-
-def thumbWebPath(rel: str)->str:
- return './files/' + rel
-
-@dataclass
-class VideoFileStore:
- top: Path
-
- def findInDir(self, subdir: str) -> Iterable[VideoFile]:
- if subdir[0] != '/': raise ValueError
- here = self.top / subdir[1:]
- manifests = list(here.glob('*.mpd'))
- if manifests:
- p = manifests[0]
- label = p.parent.name
- yield vf(p, label)
- return
- for p in sorted(list(here.glob('*.mp4')) + list(here.glob('*.webm'))):
- label = re.sub(r' \[[^\]]+\]\.\w+', '', p.name)
- yield vf(p, label)
-
-
- def findSubdirs(self, subdir: str) -> Iterable:
- if subdir[0] != '/': raise ValueError
- here = self.top / subdir[1:]
- for p in here.iterdir():
- if p.is_dir() and p.name not in {'_thumb'}:
- yield {'label': p.name, 'path': '/' + str(p.relative_to(self.top))}
-
- def thumbPath(self, vf: VideoFile) -> str:
- sha256 = hashlib.sha256()
- with open(vf.diskPath, 'rb') as f:
- firstMb = f.read(1<<20)
- sha256.update(firstMb)
- cksum = sha256.hexdigest()
- return f'_thumb/{cksum}.jpg'
-
- async def getOrCreateThumb(self, vf: VideoFile) -> str:
- p = self.top / self.thumbPath(vf)
- if not p.exists():
- sp = asyncio.create_subprocess_exec('ffmpegthumbnailer',
- '-s', '250',
- '-i', str(vf.diskPath),
- '-o', str(p))
- await sp
- return thumbWebPath(str(p.relative_to(self.top)))
-
def root(req):
return HTMLResponse("api")
@@ -85,16 +33,46 @@
list(store.findSubdirs(subdir)),
})
+
+async def ingestVideoUrl(req: Request) -> Response:
+ url = await req.body()
+ svc.ingestUrl(url)
+ return Response(status_code=202)
+
+
+async def ingestVideoUpload(req: Request) -> Response:
+ name = req.query_params['name']
+ await svc.addContent(name, req.body())
+ return Response(status_code=200)
+
+
+async def ingestQueue(req: Request) -> EventSourceResponse:
+
+ def convertEvents(svcEvents):
+ for ev in svcEvents:
+ yield dict(type='ev')
+
+ return EventSourceResponse(convertEvents(svc.events()))
+
+
store = VideoFileStore(top=Path('/data'))
+svc = VideoIngest(store)
+
def main():
-
app = Starlette(
debug=True,
routes=[
Route('/video/api/', root),
Route('/video/api/videos', videos),
+ Route('/video/api/ingest/videoUpload',
+ ingestVideoUpload,
+ methods=['POST']),
+ Route('/video/api/ingest/videoUrl',
+ ingestVideoUrl,
+ methods=['POST']),
+ Route('/video/api/ingest/queue', ingestQueue),
],
)
diff -r 4f337f9ab80e -r 53d99454f394 video_file_store.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/video_file_store.py Sat Apr 15 17:23:03 2023 -0700
@@ -0,0 +1,76 @@
+import asyncio
+import hashlib
+import re
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Iterable, Iterator, NewType
+
+
+@dataclass
+class VideoFile:
+ diskPath: Path
+ webRelPath: str
+ label: str
+ # perms, playlists, req by/when
+
+
+def vf(p: Path, label: str):
+ return VideoFile(p, './files/' + str(p.relative_to('/data')), label)
+
+
+def thumbWebPath(rel: str) -> str:
+ return './files/' + rel
+
+
+@dataclass
+class VideoFileStore:
+ top: Path
+
+ def findInDir(self, subdir: str) -> Iterable[VideoFile]:
+ if subdir[0] != '/': raise ValueError
+ here = self.top / subdir[1:]
+ manifests = list(here.glob('*.mpd'))
+ if manifests:
+ p = manifests[0]
+ label = p.parent.name
+ yield vf(p, label)
+ return
+ for p in sorted(list(here.glob('*.mp4')) + list(here.glob('*.webm'))):
+ label = re.sub(r' \[[^\]]+\]\.\w+', '', p.name)
+ yield vf(p, label)
+
+ def findSubdirs(self, subdir: str) -> Iterable:
+ if subdir[0] != '/': raise ValueError
+ here = self.top / subdir[1:]
+ for p in here.iterdir():
+ if p.is_dir() and p.name not in {'_thumb'}:
+ yield {
+ 'label': p.name,
+ 'path': '/' + str(p.relative_to(self.top))
+ }
+
+ def thumbPath(self, vf: VideoFile) -> str:
+ sha256 = hashlib.sha256()
+ with open(vf.diskPath, 'rb') as f:
+ firstMb = f.read(1 << 20)
+ sha256.update(firstMb)
+ cksum = sha256.hexdigest()
+ return f'_thumb/{cksum}.jpg'
+
+ async def getOrCreateThumb(self, vf: VideoFile) -> str:
+ p = self.top / self.thumbPath(vf)
+ if not p.exists():
+ sp = asyncio.create_subprocess_exec('ffmpegthumbnailer',
+ '-s', '250', '-i',
+ str(vf.diskPath), '-o', str(p))
+ await sp
+ return thumbWebPath(str(p.relative_to(self.top)))
+
+ async def save(self, name: str, chunks: Iterator[bytes]):
+ p = self.top / name
+ if p.exists():
+ raise ValueError(f'{p} exists')
+ data = b''
+ for c in chunks:
+ data += c
+ p.write_bytes(data)