diff --git a/README.md b/README.md index e38dc3e..12df3ec 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ file / environment rather than relying on `.env`. | `/` | Main page with all videos categorized by camera view | | `/camera/` | All videos for a single camera view | | `/video/` | Streams a video file from the TeslaCam directory | +| `/refresh` | Forces a re-scan of the TeslaCam directory | ## Camera Views @@ -119,6 +120,9 @@ file / environment rather than relying on `.env`. ## Notes +- The directory scan is cached in memory for `SCAN_CACHE_TTL` seconds + (default 300) to avoid re-walking the tree on every request. Hit + `/refresh` to force a re-scan after copying new clips onto the drive. - Debug mode is off by default (`FLASK_DEBUG=0`) and is only used by the local dev server. It has no effect when running under a WSGI server like gunicorn. - The TeslaCam directory must be accessible from the machine running the app (e.g., a mounted USB drive or network share). diff --git a/templates/index.html b/templates/index.html index 12706f3..77db409 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1029,18 +1029,31 @@ } }); - // Auto-play next clip when video finishes + // Auto-play next clip of the same camera when video finishes player.addEventListener("ended", function () { - const list = getFiltered(); - const i = list.findIndex(function (c) { + if (!current) return; + // Build a list of clips for the SAME camera, sorted like the sidebar + const sameCam = clips + .filter(function (c) { + return c.cam === current.cam; + }) + .sort(function (a, b) { + if (sortMode === "name") + return a.filename.localeCompare(b.filename); + const da = tsToDate(a.timestamp), + db = tsToDate(b.timestamp); + const ta = da ? da.getTime() : 0, + tb = db ? db.getTime() : 0; + return sortMode === "oldest" ? ta - tb : tb - ta; + }); + const i = sameCam.findIndex(function (c) { return ( - current && c.cam === current.cam && c.timestamp === current.timestamp && c.filename === current.filename ); }); - if (i > -1 && list[i + 1]) playClip(list[i + 1]); + if (i > -1 && sameCam[i + 1]) playClip(sameCam[i + 1]); }); render(); diff --git a/videoplayer.py b/videoplayer.py index 179bd04..160bd03 100644 --- a/videoplayer.py +++ b/videoplayer.py @@ -11,6 +11,7 @@ play videos from different camera views. import os import re +import time from dotenv import load_dotenv from flask import Flask, render_template, send_from_directory, abort @@ -39,8 +40,15 @@ CAMERA_VIEW_MAPPING = { 'right_pillar': 'Right Pillar' } +# In-memory scan cache. Scanning a large TeslaCam tree on every request is +# expensive, so the result is cached and only refreshed after this many seconds +# (or when /refresh is hit). Override via the SCAN_CACHE_TTL env var. +SCAN_CACHE_TTL = int(os.environ.get('SCAN_CACHE_TTL', '3600')) -def scan_teslacam_videos(): +_scan_cache = {'data': None, 'expires_at': 0.0} + + +def _scan_teslacam_videos_uncached(): """ Recursively scans the TeslaCam directory tree for video files and categorizes them by camera view. @@ -84,6 +92,22 @@ def scan_teslacam_videos(): return videos_by_camera +def scan_teslacam_videos(force_refresh=False): + """ + Returns the cached TeslaCam video scan, refreshing it when the TTL expires + or when `force_refresh` is True. Safe to call on every request. + """ + now = time.time() + if ( + _scan_cache['data'] is None + or now >= _scan_cache['expires_at'] + or force_refresh + ): + _scan_cache['data'] = _scan_teslacam_videos_uncached() + _scan_cache['expires_at'] = now + SCAN_CACHE_TTL + return _scan_cache['data'] + + def get_camera_display_name(camera_view): """Returns human-readable title for a camera view key.""" return CAMERA_VIEW_MAPPING.get(camera_view, camera_view.replace('_', ' ').title()) @@ -137,6 +161,16 @@ def serve_video(filepath): return send_from_directory(TESLACAM_DIRECTORY, filepath) +@app.route('/refresh') +def refresh(): + """ + Forces a re-scan of the TeslaCam directory, bypassing the cache TTL. + Useful after copying new clips onto the drive. + """ + scan_teslacam_videos(force_refresh=True) + return 'Cache refreshed', 200 + + if __name__ == '__main__': # Local dev server. For production, run behind a WSGI server instead, e.g.: # gunicorn -w 4 -b 0.0.0.0:5000 'videoplayer:app'