changeset 37:7cacfae58430

thumbnails rewrite - store in db; don't use YT-provided pics for now
author drewp@bigasterisk.com
date Tue, 03 Dec 2024 19:28:11 -0800
parents ed16fdbb3996
children 0aea9e55899b
files ingest.py src/VideoPage.ts thumbnail.py video.py video_file_store.py
diffstat 5 files changed, 72 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/ingest.py	Tue Dec 03 00:08:22 2024 -0800
+++ b/ingest.py	Tue Dec 03 19:28:11 2024 -0800
@@ -16,6 +16,7 @@
   durationSec
 """
 
+import asyncio
 import logging
 from pathlib import Path
 import re
@@ -25,6 +26,7 @@
 import pymongo.database
 import pymongo.collection
 from mongo_required import open_mongo_or_die
+import thumbnail
 
 logging.basicConfig(level=logging.INFO)
 log = logging.getLogger()
@@ -89,18 +91,26 @@
             for fn in files:
                 p = root / fn
                 if p.suffix not in VIDEO_EXTNS:
-                    if p.suffix == '.webp':
+                    if p.suffix in ['.jpg','.webp']:
                         # youtube thumbnail is ok in here
                         continue
                     log.info(f'ignoring {p=} {p.suffix=}')
                     continue
                 _updateOneFile(p, fs, source)
 
-
-# thumb = db.get_collection('thumb')
-# probe = db.get_collection('probe')
+async def updateThumbnails(db: pymongo.database.Database):
+    fs = db.get_collection('fs')
+    thumb = db.get_collection('thumb')
+    n=0
+    for doc in fs.find({'type': 'file'}):
+        n+=1
+        # if n>10:
+        #     log.info('updateThumbnails: stop')
+        #     break
+        await thumbnail.createThumbnail(thumb, doc['diskPath'])
 
 if __name__ == '__main__':
     while True:
         updateFs(db, sources)
+        asyncio.run(updateThumbnails(db))
         time.sleep(600)
--- a/src/VideoPage.ts	Tue Dec 03 00:08:22 2024 -0800
+++ b/src/VideoPage.ts	Tue Dec 03 19:28:11 2024 -0800
@@ -91,6 +91,9 @@
     `,
   ];
   render() {
+    const thumbSrc = (v: VideoFile) => {
+      return '/video/api/thumbnail?webRelPath='+encodeURIComponent(v.webRelPath);
+    };
     return html`
       <sl-breadcrumb>
         ${this.pathSegs.map(
@@ -105,7 +108,7 @@
       <div class="listing">
       ${this.subdirs.map((s) => html`<div class="subdir"><a href="${"./?" + subdirQuery(s.path)}">${s.label}</a></div>`)}
       ${this.videos.map(
-        (v) => html`<video-section @playVideo=${this.playVideo} thumbRelPath="${v.thumbRelPath}" title="${v.label}" manifest="/video/files/${v.webDataPath}"></video-section>`
+        (v) => html`<video-section @playVideo=${this.playVideo} thumbRelPath=${thumbSrc(v)} title="${v.label}" manifest="/video/files/${v.webDataPath}"></video-section>`
       )}
       </div>
       <p><a href="ingest/">Add new videos...</a></p>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thumbnail.py	Tue Dec 03 19:28:11 2024 -0800
@@ -0,0 +1,32 @@
+import asyncio
+from tempfile import NamedTemporaryFile
+import pymongo.collection
+import logging 
+log = logging.getLogger('thumb')
+
+async def getThumbnailData(coll: pymongo.collection.Collection, diskPath: str) -> bytes:
+    doc=coll.find_one({'diskPath':diskPath})
+    if doc is None:
+        raise Exception(f"no thumb found for {diskPath}")
+    return doc['thumbData']
+
+async def createThumbnail(coll: pymongo.collection.Collection, diskPath: str):
+    if coll.find_one({'diskPath':diskPath}):
+        return
+        coll.delete_one({'diskPath':diskPath})
+
+    # diskPath could be a YT sidecar file?
+
+    thumbFile = NamedTemporaryFile(suffix='.jpg')
+    log.info(f'createThumbnail: {diskPath=} to {thumbFile.name=}')
+    proc  = await asyncio.create_subprocess_exec('ffmpegthumbnailer',
+                                        '-s', '250', '-i',
+                                        diskPath, '-o', thumbFile.name)
+    await proc.wait()
+    if proc.returncode != 0:
+        log.error(f'createThumbnail: {proc.returncode=}')
+        return
+    thumbFile.seek(0)
+    imgData = thumbFile.read()
+    log.info(f'createThumbnail: {len(imgData)=}')
+    coll.insert_one({'diskPath':diskPath,'thumbData':imgData})
--- a/video.py	Tue Dec 03 00:08:22 2024 -0800
+++ b/video.py	Tue Dec 03 19:28:11 2024 -0800
@@ -1,7 +1,7 @@
 import asyncio
+from functools import partial
 import json
 import logging
-from pathlib import Path
 
 import uvicorn
 from prometheus_client import Gauge
@@ -16,6 +16,8 @@
 from video_file_store import VideoFileStore
 from video_ingest import VideoIngest
 from mongo_required import open_mongo_or_die
+import pymongo.database
+import thumbnail
 
 logging.basicConfig(level=logging.DEBUG)
 log = logging.getLogger()
@@ -66,6 +68,22 @@
 
     return EventSourceResponse(g())
 
+def getDiskPath(fs, webRelPath):
+    doc = fs.find_one({'webRelPath': webRelPath})
+    if doc is None:
+        raise ValueError
+    return doc['diskPath']
+
+async def getThumbnail(db: pymongo.database.Database, req: Request) -> Response:
+    webRelPath = req.query_params['webRelPath']
+    fs = db.get_collection('fs')
+    diskPath = getDiskPath(fs, webRelPath)
+    th = db.get_collection('thumb')
+    async with asyncio.timeout(10):
+        data = await thumbnail.getThumbnailData(th, diskPath)
+        return Response(content=data, media_type='image/jpeg')
+    
+
 
 db = open_mongo_or_die().get_database('video')
 store = VideoFileStore(db.get_collection('fs'))
@@ -87,6 +105,7 @@
                   ingestVideoUrl,
                   methods=['POST']),
             Route('/video/api/ingest/queue', ingestQueue),
+            Route('/video/api/thumbnail', partial(getThumbnail, db)),
         ],
     )
 
@@ -97,7 +116,6 @@
     app.state.processTask = asyncio.create_task(dl_queue.process())
     return app
 
-
 uvicorn.run(main,
             host="0.0.0.0",
             port=8004,
--- a/video_file_store.py	Tue Dec 03 00:08:22 2024 -0800
+++ b/video_file_store.py	Tue Dec 03 19:28:11 2024 -0800
@@ -29,7 +29,7 @@
         for doc in self.fs.find({
                 'type': 'file',
                 'webRelParent': webRelParent
-        }):
+        }, sort=[('label', 1)]):
             yield VideoFile(Path(doc['diskPath']), doc['webRelPath'],
                             doc['webDataPath'], doc['label'])
 
@@ -39,31 +39,12 @@
                 'dir',
                 'webRelParent':
                 '.' if subdir == '/' else subdir
-        }):
+        }, sort=[('label', 1)]):
             yield {
                 'label': doc['label'],
                 'path': doc['webRelPath'],
             }
 
-    def thumbPath(self, vf: VideoFile) -> str:
-        return '_thumb/' + vf.webRelPath
-        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:
-        raise
-        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]):
         raise
         p = self.top / name