changeset 15:53d99454f394

support dropping url or file
author drewp@bigasterisk.com
date Sat, 15 Apr 2023 17:23:03 -0700
parents 4f337f9ab80e
children 838eb0223bdb
files Dockerfile pyproject.toml skaffold.yaml src/ingest/IngestDrop.ts video.py video_file_store.py
diffstat 6 files changed, 163 insertions(+), 64 deletions(-) [+]
line wrap: on
line diff
--- 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 ./
--- 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" }
--- 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:
--- 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`
-    <div id="drop">Drop video urls here</div>
-    `;
+    return html` <div id="drop" @dragover=${this.dragover} @dragleave=${this.leave} @drop=${this.drop}>Drop video url here</div> `;
+  }
+  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,
+      });
+    }
   }
 }
--- 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),
         ],
     )
 
--- /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)