retold-remote 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/css/retold-remote.css +3 -0
  2. package/docs/README.md +181 -0
  3. package/docs/_cover.md +14 -0
  4. package/docs/_sidebar.md +10 -0
  5. package/docs/_topbar.md +3 -0
  6. package/docs/audio-viewer.md +133 -0
  7. package/docs/ebook-reader.md +90 -0
  8. package/docs/image-viewer.md +90 -0
  9. package/docs/server-setup.md +262 -0
  10. package/docs/video-viewer.md +134 -0
  11. package/html/docs.html +59 -0
  12. package/html/index.html +1 -1
  13. package/package.json +20 -6
  14. package/source/Pict-Application-RetoldRemote.js +122 -2
  15. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  16. package/source/cli/RetoldRemote-Server-Setup.js +211 -67
  17. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  18. package/source/providers/Pict-Provider-AISortManager.js +456 -0
  19. package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
  20. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  21. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  22. package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
  23. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  24. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  27. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  28. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  29. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  30. package/source/server/RetoldRemote-AISortService.js +879 -0
  31. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  32. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  33. package/source/server/RetoldRemote-CollectionService.js +843 -0
  34. package/source/server/RetoldRemote-EbookService.js +7 -16
  35. package/source/server/RetoldRemote-FileOperationService.js +560 -0
  36. package/source/server/RetoldRemote-MediaService.js +15 -14
  37. package/source/server/RetoldRemote-MetadataCache.js +411 -0
  38. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  39. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  40. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  41. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  42. package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
  43. package/source/views/PictView-Remote-Gallery.js +237 -44
  44. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  45. package/source/views/PictView-Remote-Layout.js +411 -20
  46. package/source/views/PictView-Remote-MediaViewer.js +311 -28
  47. package/source/views/PictView-Remote-SettingsPanel.js +275 -133
  48. package/source/views/PictView-Remote-TopBar.js +701 -14
  49. package/source/views/PictView-Remote-VLCSetup.js +8 -39
  50. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  51. package/web-application/css/docuserve.css +73 -0
  52. package/web-application/css/retold-remote.css +3 -0
  53. package/web-application/docs/README.md +181 -0
  54. package/web-application/docs/_cover.md +14 -0
  55. package/web-application/docs/_sidebar.md +10 -0
  56. package/web-application/docs/_topbar.md +3 -0
  57. package/web-application/docs/audio-viewer.md +133 -0
  58. package/web-application/docs/ebook-reader.md +90 -0
  59. package/web-application/docs/image-viewer.md +90 -0
  60. package/web-application/docs/server-setup.md +262 -0
  61. package/web-application/docs/video-viewer.md +134 -0
  62. package/web-application/docs.html +59 -0
  63. package/web-application/index.html +1 -1
  64. package/web-application/js/pict-docuserve.min.js +58 -0
  65. package/web-application/js/pict.min.js +2 -2
  66. package/web-application/js/pict.min.js.map +1 -1
  67. package/web-application/retold-remote.js +2652 -970
  68. package/web-application/retold-remote.js.map +1 -1
  69. package/web-application/retold-remote.min.js +60 -13
  70. package/web-application/retold-remote.min.js.map +1 -1
  71. package/server.js +0 -43
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Retold Remote -- Parime Cache Adapter
3
+ *
4
+ * Wraps parime's ParimeBinaryStorage as a unified cache layer for
5
+ * retold-remote. Supports two modes:
6
+ *
7
+ * 1. Embedded (default): ParimeBinaryStorage accessed directly in-process.
8
+ * 2. Remote: If fable.settings.ParimeCacheServer is a URL, all cache
9
+ * operations route through HTTP to a remote parime server.
10
+ *
11
+ * This adapter provides helper methods for cache key generation and
12
+ * passthrough access to the underlying storage.
13
+ */
14
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
15
+ const libCrypto = require('crypto');
16
+
17
+ class RetoldRemoteParimeCache extends libFableServiceProviderBase
18
+ {
19
+ constructor(pFable, pOptions, pServiceHash)
20
+ {
21
+ super(pFable, pOptions, pServiceHash);
22
+
23
+ this.serviceType = 'RetoldRemoteParimeCache';
24
+
25
+ this._remoteURL = (typeof(this.fable.settings.ParimeCacheServer) === 'string'
26
+ && this.fable.settings.ParimeCacheServer.length > 0)
27
+ ? this.fable.settings.ParimeCacheServer.replace(/\/+$/, '')
28
+ : null;
29
+ }
30
+
31
+ /**
32
+ * Whether this adapter is operating in remote mode.
33
+ *
34
+ * @returns {boolean}
35
+ */
36
+ get isRemote()
37
+ {
38
+ return this._remoteURL !== null;
39
+ }
40
+
41
+ /**
42
+ * Build a cache key from an array of components.
43
+ *
44
+ * Components are joined with ':' and hashed with SHA-256.
45
+ * Optionally truncate to pHashLength characters.
46
+ *
47
+ * @param {Array<string|number>} pComponents - Values to include in the key.
48
+ * @param {number} [pHashLength] - Truncate the hash to this many hex chars.
49
+ * @returns {string} The hex hash.
50
+ */
51
+ buildCacheKey(pComponents, pHashLength)
52
+ {
53
+ let tmpInput = pComponents.join(':');
54
+ let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex');
55
+ if (typeof(pHashLength) === 'number' && pHashLength > 0)
56
+ {
57
+ return tmpHash.substring(0, pHashLength);
58
+ }
59
+ return tmpHash;
60
+ }
61
+
62
+ /**
63
+ * Get the underlying ParimeBinaryStorage service.
64
+ *
65
+ * @returns {object} The ParimeBinaryStorage instance.
66
+ */
67
+ get storage()
68
+ {
69
+ return this.fable.ParimeBinaryStorage;
70
+ }
71
+
72
+ /**
73
+ * Resolve a category + key to an absolute filesystem path.
74
+ *
75
+ * Only valid in embedded mode (not remote).
76
+ *
77
+ * @param {string} pCategory - The cache category.
78
+ * @param {string} pKey - The cache key.
79
+ * @returns {string} Absolute file path.
80
+ */
81
+ resolvePath(pCategory, pKey)
82
+ {
83
+ return this.fable.ParimeBinaryStorage.resolvePath(pCategory, pKey);
84
+ }
85
+
86
+ /**
87
+ * Check if a cached item exists.
88
+ *
89
+ * @param {string} pCategory - The cache category.
90
+ * @param {string} pKey - The cache key.
91
+ * @param {function} fCallback - Callback(pError, pExists).
92
+ */
93
+ exists(pCategory, pKey, fCallback)
94
+ {
95
+ if (this._remoteURL)
96
+ {
97
+ return this._remoteExists(pCategory, pKey, fCallback);
98
+ }
99
+ return this.fable.ParimeBinaryStorage.exists(pCategory, pKey, fCallback);
100
+ }
101
+
102
+ /**
103
+ * Read a cached item.
104
+ *
105
+ * @param {string} pCategory - The cache category.
106
+ * @param {string} pKey - The cache key.
107
+ * @param {function} fCallback - Callback(pError, pBuffer).
108
+ */
109
+ read(pCategory, pKey, fCallback)
110
+ {
111
+ if (this._remoteURL)
112
+ {
113
+ return this._remoteRead(pCategory, pKey, fCallback);
114
+ }
115
+ return this.fable.ParimeBinaryStorage.read(pCategory, pKey, fCallback);
116
+ }
117
+
118
+ /**
119
+ * Write a cached item.
120
+ *
121
+ * @param {string} pCategory - The cache category.
122
+ * @param {string} pKey - The cache key.
123
+ * @param {Buffer} pBuffer - The data to write.
124
+ * @param {function} fCallback - Callback(pError).
125
+ */
126
+ write(pCategory, pKey, pBuffer, fCallback)
127
+ {
128
+ if (this._remoteURL)
129
+ {
130
+ return this._remoteWrite(pCategory, pKey, pBuffer, fCallback);
131
+ }
132
+ return this.fable.ParimeBinaryStorage.write(pCategory, pKey, pBuffer, fCallback);
133
+ }
134
+
135
+ /**
136
+ * Get a readable stream for a cached item.
137
+ *
138
+ * Only valid in embedded mode.
139
+ *
140
+ * @param {string} pCategory - The cache category.
141
+ * @param {string} pKey - The cache key.
142
+ * @param {object} [pOptions] - Stream options ({ start, end }).
143
+ * @returns {ReadStream}
144
+ */
145
+ readStream(pCategory, pKey, pOptions)
146
+ {
147
+ return this.fable.ParimeBinaryStorage.readStream(pCategory, pKey, pOptions);
148
+ }
149
+
150
+ /**
151
+ * Get file stats for a cached item.
152
+ *
153
+ * @param {string} pCategory - The cache category.
154
+ * @param {string} pKey - The cache key.
155
+ * @param {function} fCallback - Callback(pError, pStats).
156
+ */
157
+ stat(pCategory, pKey, fCallback)
158
+ {
159
+ if (this._remoteURL)
160
+ {
161
+ return this._remoteStat(pCategory, pKey, fCallback);
162
+ }
163
+ return this.fable.ParimeBinaryStorage.stat(pCategory, pKey, fCallback);
164
+ }
165
+
166
+ /**
167
+ * Delete a cached item.
168
+ *
169
+ * @param {string} pCategory - The cache category.
170
+ * @param {string} pKey - The cache key.
171
+ * @param {function} fCallback - Callback(pError).
172
+ */
173
+ delete(pCategory, pKey, fCallback)
174
+ {
175
+ if (this._remoteURL)
176
+ {
177
+ return this._remoteDelete(pCategory, pKey, fCallback);
178
+ }
179
+ return this.fable.ParimeBinaryStorage.delete(pCategory, pKey, fCallback);
180
+ }
181
+
182
+ // ── Remote mode helpers ──────────────────────────────────────
183
+
184
+ /**
185
+ * @private
186
+ */
187
+ _remoteExists(pCategory, pKey, fCallback)
188
+ {
189
+ let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}/Stat`;
190
+ let libHTTP = require('http');
191
+ let tmpParsed = new URL(tmpURL);
192
+
193
+ let tmpReq = libHTTP.request(
194
+ {
195
+ hostname: tmpParsed.hostname,
196
+ port: tmpParsed.port,
197
+ path: tmpParsed.pathname,
198
+ method: 'GET'
199
+ },
200
+ (pResponse) =>
201
+ {
202
+ let tmpChunks = [];
203
+ pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
204
+ pResponse.on('end', () =>
205
+ {
206
+ return fCallback(null, pResponse.statusCode === 200);
207
+ });
208
+ });
209
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
210
+ tmpReq.end();
211
+ }
212
+
213
+ /**
214
+ * @private
215
+ */
216
+ _remoteRead(pCategory, pKey, fCallback)
217
+ {
218
+ let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
219
+ let libHTTP = require('http');
220
+ let tmpParsed = new URL(tmpURL);
221
+
222
+ let tmpReq = libHTTP.request(
223
+ {
224
+ hostname: tmpParsed.hostname,
225
+ port: tmpParsed.port,
226
+ path: tmpParsed.pathname,
227
+ method: 'GET'
228
+ },
229
+ (pResponse) =>
230
+ {
231
+ if (pResponse.statusCode === 404)
232
+ {
233
+ pResponse.resume();
234
+ return fCallback(null, null);
235
+ }
236
+ let tmpChunks = [];
237
+ pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
238
+ pResponse.on('end', () =>
239
+ {
240
+ return fCallback(null, Buffer.concat(tmpChunks));
241
+ });
242
+ });
243
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
244
+ tmpReq.end();
245
+ }
246
+
247
+ /**
248
+ * @private
249
+ */
250
+ _remoteWrite(pCategory, pKey, pBuffer, fCallback)
251
+ {
252
+ let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
253
+ let libHTTP = require('http');
254
+ let tmpParsed = new URL(tmpURL);
255
+
256
+ let tmpReq = libHTTP.request(
257
+ {
258
+ hostname: tmpParsed.hostname,
259
+ port: tmpParsed.port,
260
+ path: tmpParsed.pathname,
261
+ method: 'PUT',
262
+ headers: { 'Content-Type': 'application/octet-stream' }
263
+ },
264
+ (pResponse) =>
265
+ {
266
+ pResponse.resume();
267
+ pResponse.on('end', () =>
268
+ {
269
+ return fCallback(pResponse.statusCode >= 400
270
+ ? new Error(`Remote write failed: ${pResponse.statusCode}`)
271
+ : null);
272
+ });
273
+ });
274
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
275
+ tmpReq.write(pBuffer);
276
+ tmpReq.end();
277
+ }
278
+
279
+ /**
280
+ * @private
281
+ */
282
+ _remoteStat(pCategory, pKey, fCallback)
283
+ {
284
+ let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}/Stat`;
285
+ let libHTTP = require('http');
286
+ let tmpParsed = new URL(tmpURL);
287
+
288
+ let tmpReq = libHTTP.request(
289
+ {
290
+ hostname: tmpParsed.hostname,
291
+ port: tmpParsed.port,
292
+ path: tmpParsed.pathname,
293
+ method: 'GET'
294
+ },
295
+ (pResponse) =>
296
+ {
297
+ let tmpChunks = [];
298
+ pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
299
+ pResponse.on('end', () =>
300
+ {
301
+ if (pResponse.statusCode === 404)
302
+ {
303
+ return fCallback(null, null);
304
+ }
305
+ try
306
+ {
307
+ let tmpBody = JSON.parse(Buffer.concat(tmpChunks).toString());
308
+ return fCallback(null, { size: tmpBody.Size });
309
+ }
310
+ catch (pError)
311
+ {
312
+ return fCallback(pError);
313
+ }
314
+ });
315
+ });
316
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
317
+ tmpReq.end();
318
+ }
319
+
320
+ /**
321
+ * @private
322
+ */
323
+ _remoteDelete(pCategory, pKey, fCallback)
324
+ {
325
+ let tmpURL = `${this._remoteURL}/1.0/Binary/${encodeURIComponent(pCategory)}/${pKey}`;
326
+ let libHTTP = require('http');
327
+ let tmpParsed = new URL(tmpURL);
328
+
329
+ let tmpReq = libHTTP.request(
330
+ {
331
+ hostname: tmpParsed.hostname,
332
+ port: tmpParsed.port,
333
+ path: tmpParsed.pathname,
334
+ method: 'DELETE'
335
+ },
336
+ (pResponse) =>
337
+ {
338
+ pResponse.resume();
339
+ pResponse.on('end', () =>
340
+ {
341
+ return fCallback(null);
342
+ });
343
+ });
344
+ tmpReq.on('error', (pError) => { return fCallback(pError); });
345
+ tmpReq.end();
346
+ }
347
+ }
348
+
349
+ module.exports = RetoldRemoteParimeCache;
@@ -1,45 +1,67 @@
1
1
  /**
2
2
  * Retold Remote -- Filesystem Thumbnail Cache
3
3
  *
4
- * Caches generated thumbnails as files in a hidden directory under the
5
- * content root. Cache keys are derived from the file path, modification
6
- * time, and requested dimensions so that stale entries are automatically
7
- * invalidated when the source file changes.
4
+ * Caches generated thumbnails using Parime's ParimeBinaryStorage.
5
+ * Cache keys use a two-level structure: a folder hash derived from
6
+ * the source file's directory, and a file-specific hash derived from
7
+ * the filename, modification time, and requested dimensions.
8
+ *
9
+ * This means all thumbnails for images in the same folder are
10
+ * co-located under the same shard directory — browsing a gallery
11
+ * folder of 10,000 images creates entries in ONE shard directory
12
+ * rather than 10,000 separate shard paths.
13
+ *
14
+ * Stale entries are automatically invalidated when the source file
15
+ * changes (because mtime is part of the file-specific hash).
8
16
  */
9
17
  const libFs = require('fs');
10
18
  const libPath = require('path');
11
19
  const libCrypto = require('crypto');
12
20
 
21
+ const _CATEGORY = 'thumbnails';
22
+
13
23
  class ThumbnailCache
14
24
  {
15
25
  /**
16
- * @param {string} pCachePath - Absolute path to the cache directory
26
+ * @param {object} pFable - The Fable instance (must have ParimeBinaryStorage wired)
17
27
  */
18
- constructor(pCachePath)
28
+ constructor(pFable)
19
29
  {
20
- this._cachePath = pCachePath;
21
-
22
- // Ensure the cache directory exists
23
- if (!libFs.existsSync(this._cachePath))
24
- {
25
- libFs.mkdirSync(this._cachePath, { recursive: true });
26
- }
30
+ this._fable = pFable;
31
+ this._storage = pFable.ParimeBinaryStorage;
27
32
  }
28
33
 
29
34
  /**
30
35
  * Build a cache key from the source file path, its mtime, and the
31
36
  * requested thumbnail dimensions.
32
37
  *
38
+ * The key has two parts separated by a slash:
39
+ * {folderHash}/{fileHash}
40
+ *
41
+ * The folder hash groups all thumbnails from the same directory
42
+ * together so they land in the same shard directory on disk.
43
+ * The file hash uniquely identifies a particular file at a
44
+ * particular mtime and dimension.
45
+ *
33
46
  * @param {string} pFilePath - Relative path to the source file
34
47
  * @param {number} pMtime - Source file mtime (ms since epoch)
35
48
  * @param {number} pWidth - Thumbnail width
36
49
  * @param {number} pHeight - Thumbnail height
37
- * @returns {string} A hex hash suitable for use as a filename
50
+ * @returns {string} A composite key "{folderHash}/{fileHash}"
38
51
  */
39
52
  buildKey(pFilePath, pMtime, pWidth, pHeight)
40
53
  {
41
- let tmpInput = `${pFilePath}:${pMtime}:${pWidth}x${pHeight}`;
42
- return libCrypto.createHash('sha256').update(tmpInput).digest('hex');
54
+ let tmpDir = libPath.dirname(pFilePath);
55
+ let tmpFile = libPath.basename(pFilePath);
56
+
57
+ // Folder hash — first 16 hex chars; used for sharding and co-location
58
+ let tmpFolderHash = libCrypto.createHash('sha256').update(tmpDir).digest('hex').substring(0, 16);
59
+
60
+ // File-specific hash — full 64 hex chars; unique per file + dimensions
61
+ let tmpFileInput = `${tmpFile}:${pMtime}:${pWidth}x${pHeight}`;
62
+ let tmpFileHash = libCrypto.createHash('sha256').update(tmpFileInput).digest('hex');
63
+
64
+ return `${tmpFolderHash}/${tmpFileHash}`;
43
65
  }
44
66
 
45
67
  /**
@@ -52,7 +74,8 @@ class ThumbnailCache
52
74
  */
53
75
  get(pKey, pFormat)
54
76
  {
55
- let tmpPath = libPath.join(this._cachePath, `${pKey}.${pFormat || 'webp'}`);
77
+ let tmpFileKey = `${pKey}.${pFormat || 'webp'}`;
78
+ let tmpPath = this._storage.resolvePath(_CATEGORY, tmpFileKey);
56
79
  if (libFs.existsSync(tmpPath))
57
80
  {
58
81
  return tmpPath;
@@ -70,19 +93,28 @@ class ThumbnailCache
70
93
  */
71
94
  put(pKey, pBuffer, pFormat)
72
95
  {
73
- let tmpPath = libPath.join(this._cachePath, `${pKey}.${pFormat || 'webp'}`);
96
+ let tmpFileKey = `${pKey}.${pFormat || 'webp'}`;
97
+ let tmpPath = this._storage.resolvePath(_CATEGORY, tmpFileKey);
98
+
99
+ // Ensure parent directory exists (for sharded paths)
100
+ let tmpDir = libPath.dirname(tmpPath);
101
+ if (!libFs.existsSync(tmpDir))
102
+ {
103
+ libFs.mkdirSync(tmpDir, { recursive: true });
104
+ }
105
+
74
106
  libFs.writeFileSync(tmpPath, pBuffer);
75
107
  return tmpPath;
76
108
  }
77
109
 
78
110
  /**
79
- * Get the absolute path to the cache directory.
111
+ * Get the cache category name.
80
112
  *
81
113
  * @returns {string}
82
114
  */
83
115
  getCachePath()
84
116
  {
85
- return this._cachePath;
117
+ return this._storage.resolvePath(_CATEGORY, '');
86
118
  }
87
119
  }
88
120
 
@@ -47,16 +47,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
47
47
 
48
48
  this.contentPath = libPath.resolve(this.options.ContentPath);
49
49
 
50
- this.cachePath = this.options.CachePath
51
- || libPath.join(process.cwd(), 'dist', 'retold-cache', 'video-frames');
52
-
53
- // Ensure cache directory exists
54
- if (!libFs.existsSync(this.cachePath))
55
- {
56
- libFs.mkdirSync(this.cachePath, { recursive: true });
57
- }
58
-
59
- this.fable.log.info(`Video Frame Service: cache at ${this.cachePath}`);
50
+ this.fable.log.info('Video Frame Service: using ParimeBinaryStorage (category: video-frames)');
60
51
  }
61
52
 
62
53
  /**
@@ -75,7 +66,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
75
66
  {
76
67
  let tmpInput = `${pAbsPath}:${pMtimeMs}:${pFrameCount}:${pWidth}x${pHeight}`;
77
68
  let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex').substring(0, 16);
78
- return libPath.join(this.cachePath, tmpHash);
69
+ return this.fable.ParimeBinaryStorage.resolvePath('video-frames', tmpHash);
79
70
  }
80
71
 
81
72
  /**
@@ -398,7 +389,7 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
398
389
  return fCallback(new Error('Invalid cache key.'));
399
390
  }
400
391
 
401
- let tmpCacheDir = libPath.join(this.cachePath, pCacheKey);
392
+ let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('video-frames', pCacheKey);
402
393
  if (!libFs.existsSync(tmpCacheDir))
403
394
  {
404
395
  return fCallback(new Error('Cache directory not found. Extract frames first.'));
@@ -466,11 +457,12 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
466
457
  return null;
467
458
  }
468
459
 
469
- let tmpPath = libPath.join(this.cachePath, pCacheKey, pFilename);
460
+ let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('video-frames', pCacheKey);
461
+ let tmpPath = libPath.join(tmpCacheDir, pFilename);
470
462
 
471
- // Double-check it's under our cache dir
463
+ // Double-check it's under the storage root
472
464
  let tmpResolved = libPath.resolve(tmpPath);
473
- if (!tmpResolved.startsWith(this.cachePath))
465
+ if (!tmpResolved.startsWith(this.fable.ParimeBinaryStorage.storageRoot))
474
466
  {
475
467
  return null;
476
468
  }
@@ -342,7 +342,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
342
342
  // Header
343
343
  tmpHTML += '<div class="retold-remote-aex-header">';
344
344
  tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back to audio (Esc)">&larr; Back</button>';
345
- tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer &mdash; ' + this._escapeHTML(tmpFileName) + '</div>';
345
+ tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer &mdash; ' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFileName) + '</div>';
346
346
  tmpHTML += '</div>';
347
347
 
348
348
  // Info bar (populated after waveform loads)
@@ -449,18 +449,18 @@ class RetoldRemoteAudioExplorerView extends libPictView
449
449
  if (tmpInfoBar)
450
450
  {
451
451
  let tmpInfoHTML = '';
452
- tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Duration</span> <span class="retold-remote-aex-info-value">' + this._escapeHTML(tmpData.DurationFormatted) + '</span></span>';
452
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Duration</span> <span class="retold-remote-aex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.DurationFormatted) + '</span></span>';
453
453
  if (tmpData.SampleRate)
454
454
  {
455
455
  tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Sample Rate</span> <span class="retold-remote-aex-info-value">' + (tmpData.SampleRate / 1000).toFixed(1) + ' kHz</span></span>';
456
456
  }
457
457
  if (tmpData.Channels)
458
458
  {
459
- tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Channels</span> <span class="retold-remote-aex-info-value">' + tmpData.Channels + (tmpData.ChannelLayout ? ' (' + this._escapeHTML(tmpData.ChannelLayout) + ')' : '') + '</span></span>';
459
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Channels</span> <span class="retold-remote-aex-info-value">' + tmpData.Channels + (tmpData.ChannelLayout ? ' (' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.ChannelLayout) + ')' : '') + '</span></span>';
460
460
  }
461
461
  if (tmpData.Codec)
462
462
  {
463
- tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Codec</span> <span class="retold-remote-aex-info-value">' + this._escapeHTML(tmpData.Codec) + '</span></span>';
463
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Codec</span> <span class="retold-remote-aex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.Codec) + '</span></span>';
464
464
  }
465
465
  if (tmpData.Bitrate)
466
466
  {
@@ -468,9 +468,9 @@ class RetoldRemoteAudioExplorerView extends libPictView
468
468
  }
469
469
  if (tmpData.FileSize)
470
470
  {
471
- tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Size</span> <span class="retold-remote-aex-info-value">' + this._formatFileSize(tmpData.FileSize) + '</span></span>';
471
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Size</span> <span class="retold-remote-aex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatFileSize(tmpData.FileSize) + '</span></span>';
472
472
  }
473
- tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Peaks</span> <span class="retold-remote-aex-info-value">' + tmpData.PeakCount + ' (' + this._escapeHTML(tmpData.Method) + ')</span></span>';
473
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Peaks</span> <span class="retold-remote-aex-info-value">' + tmpData.PeakCount + ' (' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.Method) + ')</span></span>';
474
474
 
475
475
  tmpInfoBar.innerHTML = tmpInfoHTML;
476
476
  tmpInfoBar.style.display = '';
@@ -964,7 +964,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
964
964
  {
965
965
  let tmpViewStartTime = this._viewStart * tmpDuration;
966
966
  let tmpViewEndTime = this._viewEnd * tmpDuration;
967
- tmpViewRangeEl.textContent = this._formatTimestamp(tmpViewStartTime) + ' - ' + this._formatTimestamp(tmpViewEndTime);
967
+ tmpViewRangeEl.textContent = this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpViewStartTime, true) + ' - ' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpViewEndTime, true);
968
968
  }
969
969
 
970
970
  // Selection
@@ -976,7 +976,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
976
976
  let tmpSelMin = Math.min(this._selectionStart, this._selectionEnd) * tmpDuration;
977
977
  let tmpSelMax = Math.max(this._selectionStart, this._selectionEnd) * tmpDuration;
978
978
  let tmpSelDur = tmpSelMax - tmpSelMin;
979
- tmpSelRangeEl.textContent = this._formatTimestamp(tmpSelMin) + ' - ' + this._formatTimestamp(tmpSelMax) + ' (' + this._formatTimestamp(tmpSelDur) + ')';
979
+ tmpSelRangeEl.textContent = this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpSelMin, true) + ' - ' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpSelMax, true) + ' (' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpSelDur, true) + ')';
980
980
  }
981
981
  else
982
982
  {
@@ -994,7 +994,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
994
994
  if (tmpWrap)
995
995
  {
996
996
  let tmpNorm = this._viewStart + (this._cursorX / tmpWrap.clientWidth) * (this._viewEnd - this._viewStart);
997
- tmpCursorEl.textContent = this._formatTimestamp(tmpNorm * tmpDuration);
997
+ tmpCursorEl.textContent = this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpNorm * tmpDuration, true);
998
998
  }
999
999
  }
1000
1000
  else
@@ -1167,45 +1167,12 @@ class RetoldRemoteAudioExplorerView extends libPictView
1167
1167
  if (tmpBody)
1168
1168
  {
1169
1169
  tmpBody.innerHTML = '<div class="retold-remote-aex-error">'
1170
- + '<div class="retold-remote-aex-error-message">' + this._escapeHTML(pMessage || 'An error occurred.') + '</div>'
1170
+ + '<div class="retold-remote-aex-error-message">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pMessage || 'An error occurred.') + '</div>'
1171
1171
  + '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()">Back to Audio</button>'
1172
1172
  + '</div>';
1173
1173
  }
1174
1174
  }
1175
1175
 
1176
- _formatTimestamp(pSeconds)
1177
- {
1178
- if (pSeconds === null || pSeconds === undefined || isNaN(pSeconds))
1179
- {
1180
- return '--';
1181
- }
1182
- let tmpHours = Math.floor(pSeconds / 3600);
1183
- let tmpMinutes = Math.floor((pSeconds % 3600) / 60);
1184
- let tmpSecs = Math.floor(pSeconds % 60);
1185
- let tmpMs = Math.floor((pSeconds % 1) * 10);
1186
-
1187
- if (tmpHours > 0)
1188
- {
1189
- return `${tmpHours}:${String(tmpMinutes).padStart(2, '0')}:${String(tmpSecs).padStart(2, '0')}.${tmpMs}`;
1190
- }
1191
- return `${tmpMinutes}:${String(tmpSecs).padStart(2, '0')}.${tmpMs}`;
1192
- }
1193
-
1194
- _escapeHTML(pText)
1195
- {
1196
- if (!pText) return '';
1197
- return pText.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1198
- }
1199
-
1200
- _formatFileSize(pBytes)
1201
- {
1202
- if (!pBytes || pBytes === 0) return '0 B';
1203
- let tmpUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
1204
- let tmpIndex = Math.floor(Math.log(pBytes) / Math.log(1024));
1205
- if (tmpIndex >= tmpUnits.length) tmpIndex = tmpUnits.length - 1;
1206
- let tmpSize = pBytes / Math.pow(1024, tmpIndex);
1207
- return tmpSize.toFixed(tmpIndex === 0 ? 0 : 1) + ' ' + tmpUnits[tmpIndex];
1208
- }
1209
1176
  }
1210
1177
 
1211
1178
  RetoldRemoteAudioExplorerView.default_configuration = _ViewConfiguration;