diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index a615b38..a6adeb0 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -13,23 +13,39 @@ POLL=60 # how often to check on the app # Upgrade yt-dlp only if PyPI has a newer version. # Returns 0 if an update was installed, 1 otherwise (incl. errors / up to date). +# Versions are compared numerically: the installed version can be zero-padded +# (e.g. 2026.06.09) while PyPI reports the normalized form (e.g. 2026.6.9). check_and_update() { - current=$(python -c "import yt_dlp; print(yt_dlp.version.__version__)" 2>/dev/null) || return 1 - latest=$(python -c "import json,urllib.request; print(json.load(urllib.request.urlopen('https://pypi.org/pypi/yt-dlp/json', timeout=30))['info']['version'])" 2>/dev/null) || { - echo "Could not reach PyPI to check for yt-dlp updates; will retry." - return 1 - } + if python3 - <<'PY' +import json, sys, urllib.request +try: + import yt_dlp + current = yt_dlp.version.__version__ + latest = json.load(urllib.request.urlopen( + 'https://pypi.org/pypi/yt-dlp/json', timeout=30))['info']['version'] +except Exception as e: + print(f"Could not check for yt-dlp updates ({e}); will retry.") + sys.exit(1) - if [ "$current" = "$latest" ]; then - echo "yt-dlp is up to date ($current)." - return 1 - fi +def key(v): + try: + return tuple(int(p) for p in v.split('.')) + except ValueError: + return None - echo "New yt-dlp available: $current -> $latest. Updating..." - if pip install --upgrade --quiet --root-user-action=ignore "yt-dlp[default,curl-cffi]"; then - return 0 +ck, lk = key(current), key(latest) +newer = (lk > ck) if (ck and lk) else (current != latest) +if newer: + print(f"New yt-dlp available: {current} -> {latest}. Updating...") + sys.exit(0) +print(f"yt-dlp is up to date ({current}).") +sys.exit(1) +PY + then + pip install --upgrade --quiet --root-user-action=ignore "yt-dlp[default,curl-cffi]" \ + && return 0 + echo "yt-dlp update failed; continuing with the installed version." fi - echo "yt-dlp update failed; continuing with the installed version." return 1 } diff --git a/frontend/css/style.css b/frontend/css/style.css index f165e55..660191e 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1431,6 +1431,14 @@ body.feed-mode-open .mode-toggle-btn .icon-svg { height: 1px; } +/* Stands in for the slides above the rendered window so scroll position and + snap points stay stable while slides are virtualized in and out. */ +.feed-top-spacer { + width: 100%; + height: 0; + flex: none; +} + .feed-slide { position: relative; width: 100%; diff --git a/frontend/index.html b/frontend/index.html index c0574bb..e72a4b0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -135,6 +135,7 @@ Unmute
+
diff --git a/frontend/js/feed.js b/frontend/js/feed.js index 3415bd1..47f651b 100644 --- a/frontend/js/feed.js +++ b/frontend/js/feed.js @@ -3,10 +3,49 @@ App.feed = App.feed || {}; (function() { const state = App.state; - let observer = null; - let sentinelObserver = null; + + // Tuning knobs for the virtualized feed. + // + // The feed is a y-scroll-snap list where every slide is exactly one + // viewport tall. We never keep the whole list in the DOM. Instead we keep + // a sliding window of slides around the active one: + // + // [active - HISTORY_COUNT .. active + RENDER_AHEAD] + // + // Everything outside that range is removed from the document; its video + // JSON stays in state.loadedVideos so the slide is rebuilt instantly when + // the user scrolls back to (or forward into) it. A top spacer absorbs the + // height of the not-yet-rendered slides above the window, so adding or + // removing slides never shifts the scroll position: any slide at index i + // always sits at scrollTop === i * slideHeight regardless of the window. + // + // Separately we prefetch PREFETCH_PAGES worth of video JSON ahead of the + // active slide so the data buffer is always full before we need to render + // a slide from it. + const PRELOAD_COUNT = 2; // slides ahead kept with a live