Explorar o código

Move Movie front-end cache flow to backend

Toby Chui hai 2 semanas
pai
achega
388c860b3b

+ 2 - 1
src/web/Movie/backend/common.js

@@ -14,7 +14,8 @@ var TRANSCODE_API  = "../media/transcode";            // ?file
 var AGI_INTERFACE = "../system/ajgi/interface?script=";
 
 // ── Script paths (used when calling ao_module_agirun from the frontend) ──────
-var SCRIPT_GET_LIBRARY   = BACKEND_PATH + "getLibrary.js";
+var SCRIPT_GET_LIBRARY       = BACKEND_PATH + "getLibrary.js";
+var SCRIPT_GET_LIBRARY_CACHE = BACKEND_PATH + "getLibraryCache.js";
 var SCRIPT_GET_EPISODES  = BACKEND_PATH + "getEpisodes.js";
 var SCRIPT_GET_THUMBNAIL = BACKEND_PATH + "getThumbnail.js";
 var SCRIPT_LIST_FOLDER   = BACKEND_PATH + "listFolder.js";

+ 13 - 0
src/web/Movie/backend/getLibrary.js

@@ -343,6 +343,19 @@ function main() {
         for (var j = 0; j < found.length; j++) { allAlbums.push(found[j]); }
     }
 
+    // Persist to server-side cache so any device (or next session) can load instantly.
+    // This runs before sendJSONResp so the file is guaranteed written even if the
+    // client closes the tab before the response arrives.
+    try {
+        if (!filelib.fileExists("user:/Document/"))           { filelib.mkdir("user:/Document/"); }
+        if (!filelib.fileExists("user:/Document/Appdata/"))   { filelib.mkdir("user:/Document/Appdata/"); }
+        if (!filelib.fileExists("user:/Document/Appdata/Movie/")) { filelib.mkdir("user:/Document/Appdata/Movie/"); }
+        filelib.writeFile(
+            "user:/Document/Appdata/Movie/library_cache.json",
+            JSON.stringify({ ts: new Date().getTime(), data: allAlbums })
+        );
+    } catch (e) {}  // never let a cache-write failure break the response
+
     sendJSONResp(JSON.stringify(allAlbums));
 }
 

+ 31 - 0
src/web/Movie/backend/getLibraryCache.js

@@ -0,0 +1,31 @@
+/*
+    Movie App - Library Cache Reader
+    Returns the previously-saved library scan result from server storage.
+    Does NO file-system scanning — responds in milliseconds.
+
+    Written by getLibrary.js after every full scan, so it is always as fresh
+    as the last completed scan (even if the browser tab was closed during it).
+
+    Returns JSON: { ts: <unix-ms>, data: [...albums] }
+    or            { error: "no_cache" }
+*/
+
+includes("common.js");
+requirelib("filelib");
+
+var CACHE_FILE = "user:/Document/Appdata/Movie/library_cache.json";
+
+function main() {
+    if (!filelib.fileExists(CACHE_FILE)) {
+        sendJSONResp(JSON.stringify({ error: "no_cache" }));
+        return;
+    }
+    var content = filelib.readFile(CACHE_FILE);
+    if (!content || content === false || content.length < 10) {
+        sendJSONResp(JSON.stringify({ error: "no_cache" }));
+        return;
+    }
+    sendJSONResp(content);  // { ts, data }  — already valid JSON
+}
+
+main();

+ 72 - 66
src/web/Movie/index.html

@@ -1170,10 +1170,10 @@ html, body {
 // ─── All configurable paths come from backend/common.js ──────────────────────
 // (SCRIPT_GET_LIBRARY, SCRIPT_GET_EPISODES, SCRIPT_GET_THUMBNAIL, MEDIA_API)
 
-// ─── Library cache (localStorage) ────────────────────────────────────────────
-var LIBRARY_CACHE_KEY     = 'movie_library_cache';
-var LIBRARY_CACHE_VERSION = 1;
-var libraryNeedsRedraw    = false; // set when bg-refresh finishes outside the library view
+// ─── Library cache ────────────────────────────────────────────────────────────
+// Cache is stored server-side (user:/Document/Appdata/Movie/library_cache.json)
+// so it is shared across devices for the same user account.
+var libraryNeedsRedraw = false; // set when bg-refresh finishes outside the library view
 
 // ─── App state ────────────────────────────────────────────────────────────────
 var library        = [];   // full album array from server
@@ -1638,29 +1638,8 @@ $(document).ready(function () {
     });
 });
 
-// ─── Library cache helpers ─────────────────────────────────────────────────────
-function getCachedLibrary() {
-    try {
-        var raw = localStorage.getItem(LIBRARY_CACHE_KEY);
-        if (!raw) { return null; }
-        var obj = JSON.parse(raw);
-        if (!obj || obj.version !== LIBRARY_CACHE_VERSION || !Array.isArray(obj.data)) { return null; }
-        return obj;  // { version, ts, data }
-    } catch (e) { return null; }
-}
-
-function updateLibraryCache(data) {
-    try {
-        localStorage.setItem(LIBRARY_CACHE_KEY, JSON.stringify({
-            version: LIBRARY_CACHE_VERSION,
-            ts:      Date.now(),
-            data:    data
-        }));
-    } catch (e) {} // storage full or unavailable — silently ignore
-}
-
-// spinning=true  →  show spinner, hide Refresh button
-// spinning=false →  hide spinner, show Refresh button
+// ─── Library status bar helpers ────────────────────────────────────────────────
+// spinning=true  shows the spinner and hides the Refresh button, and vice-versa.
 function setLibraryStatus(text, spinning) {
     $('#library-status-text').text(text);
     $('#library-spinner').toggle(spinning);
@@ -1675,67 +1654,95 @@ function timeAgo(ts) {
     return Math.floor(d / 86400) + 'd ago';
 }
 
-// Re-render the library grid, preserving an active search filter if present
+// Re-render the library grid, preserving an active search filter if present.
 function renderCurrentLibrary() {
     var q = $('#search-input').val().trim().toLowerCase();
     renderLibrary(q ? library.filter(function (a) { return a.name.toLowerCase().indexOf(q) > -1; }) : library);
 }
 
-// ─── Load library (cache-first) ────────────────────────────────────────────────
+// Run a background full scan. getLibrary.js writes the cache file server-side
+// before it sends its response, so the cache is updated even if the tab closes
+// partway through — the browser just won't receive the response in that case.
+function backgroundScanLibrary() {
+    ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
+        if (!data || data.error) {
+            setLibraryStatus('Refresh failed · showing cached data', false);
+            return;
+        }
+        library = data;
+        if ($('#view-library').hasClass('active')) {
+            renderCurrentLibrary();
+            libraryNeedsRedraw = false;
+        } else {
+            libraryNeedsRedraw = true;
+        }
+        var n = library.length;
+        setLibraryStatus(n + ' item' + (n !== 1 ? 's' : ''), false);
+    }, function () {
+        setLibraryStatus('Refresh failed · showing cached data', false);
+    });
+}
+
+// ─── Load library (server-side cache-first) ────────────────────────────────────
+// 1. Ask getLibraryCache.js for the last saved scan result (milliseconds, no scan).
+// 2a. Cache hit  → render immediately, kick off backgroundScanLibrary().
+// 2b. Cache miss → show loading overlay, run full scan, render when done.
 function loadLibrary() {
-    var cached = getCachedLibrary();
+    ao_module_agirun(SCRIPT_GET_LIBRARY_CACHE, {}, function (cached) {
 
-    if (cached) {
-        // ── Have cache: show it immediately, skip the loading overlay ──────────
-        $('#loading-overlay').hide();
-        library = cached.data;
-        renderLibrary(library);
-        setLibraryStatus('Cached · ' + timeAgo(cached.ts) + ' · refreshing…', true);
+        if (cached && !cached.error && Array.isArray(cached.data)) {
+            // Fast path: paint the UI from cache right away
+            $('#loading-overlay').hide();
+            library = cached.data;
+            renderLibrary(library);
+            setLibraryStatus('Cached · ' + timeAgo(cached.ts) + ' · refreshing…', true);
+            backgroundScanLibrary();
 
-        // Background fetch — silently update when done
-        ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
-            if (!data || data.error) {
-                setLibraryStatus('⚠ Refresh failed · showing cached data', false);
-                return;
-            }
-            updateLibraryCache(data);
-            library = data;
-            if ($('#view-library').hasClass('active')) {
-                renderCurrentLibrary();    // user is watching — update smoothly
-                libraryNeedsRedraw = false;
-            } else {
-                libraryNeedsRedraw = true; // user navigated away — re-render on return
-            }
-            var n = library.length;
-            setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : ''), false);
-        }, function () {
-            setLibraryStatus('⚠ Refresh failed · showing cached data', false);
-        });
+        } else {
+            // Cold start: no cache yet — full scan with loading overlay
+            setLibraryStatus('Loading…', true);
+            ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
+                $('#loading-overlay').fadeOut(300);
+                if (!data || data.error) {
+                    showToast('Failed to load library');
+                    setLibraryStatus('Failed to load library', false);
+                    return;
+                }
+                library = data;
+                renderLibrary(library);
+                var n = library.length;
+                setLibraryStatus(n + ' item' + (n !== 1 ? 's' : ''), false);
+            }, function () {
+                $('#loading-overlay').fadeOut(300);
+                showToast('Error loading library');
+                setLibraryStatus('Error loading library', false);
+            });
+        }
 
-    } else {
-        // ── No cache: full loading screen, wait for first fetch ───────────────
+    }, function () {
+        // getLibraryCache.js itself failed — treat as cold start
         setLibraryStatus('Loading…', true);
         ao_module_agirun(SCRIPT_GET_LIBRARY, {}, function (data) {
             $('#loading-overlay').fadeOut(300);
             if (!data || data.error) {
                 showToast('Failed to load library');
-                setLibraryStatus('Failed to load library', false);
+                setLibraryStatus('Failed to load library', false);
                 return;
             }
-            updateLibraryCache(data);
             library = data;
             renderLibrary(library);
             var n = library.length;
-            setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : ''), false);
+            setLibraryStatus(n + ' item' + (n !== 1 ? 's' : ''), false);
         }, function () {
             $('#loading-overlay').fadeOut(300);
             showToast('Error loading library');
-            setLibraryStatus('Error loading library', false);
+            setLibraryStatus('Error loading library', false);
         });
-    }
+    });
 }
 
 // ─── Manual refresh (Refresh button) ──────────────────────────────────────────
+// Triggers a full scan; getLibrary.js saves the cache file automatically.
 function refreshLibrary() {
     $('#library-refresh-btn').prop('disabled', true);
     setLibraryStatus('Refreshing…', true);
@@ -1743,19 +1750,18 @@ function refreshLibrary() {
         $('#library-refresh-btn').prop('disabled', false);
         if (!data || data.error) {
             showToast('Failed to refresh library');
-            setLibraryStatus('Refresh failed', false);
+            setLibraryStatus('Refresh failed', false);
             return;
         }
-        updateLibraryCache(data);
         library = data;
         renderCurrentLibrary();
         libraryNeedsRedraw = false;
         var n = library.length;
-        setLibraryStatus('✓ ' + n + ' item' + (n !== 1 ? 's' : '') + ' · just refreshed', false);
+        setLibraryStatus(n + ' item' + (n !== 1 ? 's' : '') + ' · just refreshed', false);
     }, function () {
         $('#library-refresh-btn').prop('disabled', false);
         showToast('Error refreshing library');
-        setLibraryStatus('Refresh failed', false);
+        setLibraryStatus('Refresh failed', false);
     });
 }