Mercurial > code > home > repos > video
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)