retold-remote 0.0.4 → 0.0.6

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 (63) hide show
  1. package/docs/README.md +181 -0
  2. package/docs/_cover.md +14 -0
  3. package/docs/_sidebar.md +10 -0
  4. package/docs/_topbar.md +3 -0
  5. package/docs/audio-viewer.md +133 -0
  6. package/docs/ebook-reader.md +90 -0
  7. package/docs/image-viewer.md +90 -0
  8. package/docs/server-setup.md +262 -0
  9. package/docs/video-viewer.md +134 -0
  10. package/html/docs.html +59 -0
  11. package/package.json +21 -7
  12. package/source/Pict-Application-RetoldRemote.js +143 -2
  13. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  14. package/source/cli/RetoldRemote-Server-Setup.js +82 -67
  15. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  16. package/source/providers/Pict-Provider-CollectionManager.js +934 -0
  17. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  18. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  19. package/source/providers/Pict-Provider-GalleryNavigation.js +270 -353
  20. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  21. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  22. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  23. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  24. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  27. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  28. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  29. package/source/server/RetoldRemote-CollectionService.js +684 -0
  30. package/source/server/RetoldRemote-EbookService.js +7 -16
  31. package/source/server/RetoldRemote-MediaService.js +3 -14
  32. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  33. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  34. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  35. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  36. package/source/views/PictView-Remote-CollectionsPanel.js +1087 -0
  37. package/source/views/PictView-Remote-Gallery.js +237 -44
  38. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  39. package/source/views/PictView-Remote-Layout.js +410 -20
  40. package/source/views/PictView-Remote-MediaViewer.js +338 -51
  41. package/source/views/PictView-Remote-SettingsPanel.js +155 -138
  42. package/source/views/PictView-Remote-TopBar.js +615 -14
  43. package/source/views/PictView-Remote-VLCSetup.js +766 -0
  44. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  45. package/web-application/css/docuserve.css +73 -0
  46. package/web-application/docs/README.md +181 -0
  47. package/web-application/docs/_cover.md +14 -0
  48. package/web-application/docs/_sidebar.md +10 -0
  49. package/web-application/docs/_topbar.md +3 -0
  50. package/web-application/docs/audio-viewer.md +133 -0
  51. package/web-application/docs/ebook-reader.md +90 -0
  52. package/web-application/docs/image-viewer.md +90 -0
  53. package/web-application/docs/server-setup.md +262 -0
  54. package/web-application/docs/video-viewer.md +134 -0
  55. package/web-application/docs.html +59 -0
  56. package/web-application/js/pict-docuserve.min.js +58 -0
  57. package/web-application/js/pict.min.js +2 -2
  58. package/web-application/js/pict.min.js.map +1 -1
  59. package/web-application/retold-remote.js +2558 -439
  60. package/web-application/retold-remote.js.map +1 -1
  61. package/web-application/retold-remote.min.js +41 -11
  62. package/web-application/retold-remote.min.js.map +1 -1
  63. package/server.js +0 -43
@@ -18,8 +18,7 @@ const libChildProcess = require('child_process');
18
18
 
19
19
  const _DefaultServiceConfiguration =
20
20
  {
21
- "ContentPath": ".",
22
- "CachePath": null
21
+ "ContentPath": "."
23
22
  };
24
23
 
25
24
  // Extensions that can be converted to EPUB by ebook-convert
@@ -60,16 +59,7 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
60
59
 
61
60
  this.contentPath = libPath.resolve(this.options.ContentPath);
62
61
 
63
- this.cachePath = this.options.CachePath
64
- || libPath.join(process.cwd(), 'dist', 'retold-cache', 'ebook-conversions');
65
-
66
- // Ensure cache directory exists
67
- if (!libFs.existsSync(this.cachePath))
68
- {
69
- libFs.mkdirSync(this.cachePath, { recursive: true });
70
- }
71
-
72
- this.fable.log.info(`Ebook Service: cache at ${this.cachePath}`);
62
+ this.fable.log.info('Ebook Service: using ParimeBinaryStorage (category: ebook-cache)');
73
63
  }
74
64
 
75
65
  /**
@@ -96,7 +86,7 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
96
86
  {
97
87
  let tmpInput = `${pAbsPath}:${pMtimeMs}`;
98
88
  let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex').substring(0, 16);
99
- return libPath.join(this.cachePath, tmpHash);
89
+ return this.fable.ParimeBinaryStorage.resolvePath('ebook-cache', tmpHash);
100
90
  }
101
91
 
102
92
  /**
@@ -221,11 +211,12 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
221
211
  return null;
222
212
  }
223
213
 
224
- let tmpPath = libPath.join(this.cachePath, pCacheKey, pFilename);
214
+ let tmpCacheDir = this.fable.ParimeBinaryStorage.resolvePath('ebook-cache', pCacheKey);
215
+ let tmpPath = libPath.join(tmpCacheDir, pFilename);
225
216
 
226
- // Double-check it's under our cache dir
217
+ // Double-check it's under the storage root
227
218
  let tmpResolved = libPath.resolve(tmpPath);
228
- if (!tmpResolved.startsWith(this.cachePath))
219
+ if (!tmpResolved.startsWith(this.fable.ParimeBinaryStorage.storageRoot))
229
220
  {
230
221
  return null;
231
222
  }
@@ -18,15 +18,11 @@ const libUrl = require('url');
18
18
  const libToolDetector = require('./RetoldRemote-ToolDetector.js');
19
19
  const libThumbnailCache = require('./RetoldRemote-ThumbnailCache.js');
20
20
 
21
- const _ImageExtensions = { 'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'webp': true, 'svg': true, 'bmp': true, 'ico': true, 'avif': true, 'tiff': true, 'tif': true, 'heic': true, 'heif': true };
22
- const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true };
23
- const _AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
24
- const _DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true };
21
+ const libExtensionMaps = require('../RetoldRemote-ExtensionMaps.js');
25
22
 
26
23
  const _DefaultServiceConfiguration =
27
24
  {
28
25
  "ContentPath": ".",
29
- "ThumbnailCachePath": null,
30
26
  "APIRoutePrefix": "/api/media",
31
27
  "DefaultThumbnailWidth": 200,
32
28
  "DefaultThumbnailHeight": 200
@@ -51,12 +47,9 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
51
47
 
52
48
  this.contentPath = libPath.resolve(this.options.ContentPath);
53
49
 
54
- let tmpCachePath = this.options.ThumbnailCachePath
55
- || libPath.join(process.cwd(), 'dist', 'retold-cache', 'thumbnails');
56
-
57
50
  this.toolDetector = new libToolDetector();
58
51
  this.capabilities = this.toolDetector.detect();
59
- this.thumbnailCache = new libThumbnailCache(tmpCachePath);
52
+ this.thumbnailCache = new libThumbnailCache(this.fable);
60
53
  this.pathRegistry = this.options.PathRegistry || null;
61
54
 
62
55
  this.fable.log.info(`Media Service: capabilities = ${JSON.stringify(this.capabilities)}`);
@@ -95,11 +88,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
95
88
  */
96
89
  _getMediaCategory(pExtension)
97
90
  {
98
- if (_ImageExtensions[pExtension]) return 'image';
99
- if (_VideoExtensions[pExtension]) return 'video';
100
- if (_AudioExtensions[pExtension]) return 'audio';
101
- if (_DocumentExtensions[pExtension]) return 'document';
102
- return 'other';
91
+ return libExtensionMaps.getCategory(pExtension);
103
92
  }
104
93
 
105
94
  /**
@@ -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
  }