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.
- package/css/retold-remote.css +3 -0
- package/docs/README.md +181 -0
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +10 -0
- package/docs/_topbar.md +3 -0
- package/docs/audio-viewer.md +133 -0
- package/docs/ebook-reader.md +90 -0
- package/docs/image-viewer.md +90 -0
- package/docs/server-setup.md +262 -0
- package/docs/video-viewer.md +134 -0
- package/html/docs.html +59 -0
- package/html/index.html +1 -1
- package/package.json +20 -6
- package/source/Pict-Application-RetoldRemote.js +122 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +211 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
- package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
- package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
- package/source/providers/Pict-Provider-ToastNotification.js +96 -0
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
- package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
- package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +843 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +15 -14
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/server/RetoldRemote-ParimeCache.js +349 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
- package/source/server/RetoldRemote-VideoFrameService.js +7 -15
- package/source/views/PictView-Remote-AudioExplorer.js +10 -43
- package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
- package/source/views/PictView-Remote-Gallery.js +237 -44
- package/source/views/PictView-Remote-ImageViewer.js +1 -34
- package/source/views/PictView-Remote-Layout.js +411 -20
- package/source/views/PictView-Remote-MediaViewer.js +311 -28
- package/source/views/PictView-Remote-SettingsPanel.js +275 -133
- package/source/views/PictView-Remote-TopBar.js +701 -14
- package/source/views/PictView-Remote-VLCSetup.js +8 -39
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/docs/README.md +181 -0
- package/web-application/docs/_cover.md +14 -0
- package/web-application/docs/_sidebar.md +10 -0
- package/web-application/docs/_topbar.md +3 -0
- package/web-application/docs/audio-viewer.md +133 -0
- package/web-application/docs/ebook-reader.md +90 -0
- package/web-application/docs/image-viewer.md +90 -0
- package/web-application/docs/server-setup.md +262 -0
- package/web-application/docs/video-viewer.md +134 -0
- package/web-application/docs.html +59 -0
- package/web-application/index.html +1 -1
- package/web-application/js/pict-docuserve.min.js +58 -0
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +2652 -970
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +60 -13
- package/web-application/retold-remote.min.js.map +1 -1
- 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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
26
|
+
* @param {object} pFable - The Fable instance (must have ParimeBinaryStorage wired)
|
|
17
27
|
*/
|
|
18
|
-
constructor(
|
|
28
|
+
constructor(pFable)
|
|
19
29
|
{
|
|
20
|
-
this.
|
|
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
|
|
50
|
+
* @returns {string} A composite key "{folderHash}/{fileHash}"
|
|
38
51
|
*/
|
|
39
52
|
buildKey(pFilePath, pMtime, pWidth, pHeight)
|
|
40
53
|
{
|
|
41
|
-
let
|
|
42
|
-
|
|
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
|
|
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
|
|
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
|
|
111
|
+
* Get the cache category name.
|
|
80
112
|
*
|
|
81
113
|
* @returns {string}
|
|
82
114
|
*/
|
|
83
115
|
getCachePath()
|
|
84
116
|
{
|
|
85
|
-
return this.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
463
|
+
// Double-check it's under the storage root
|
|
472
464
|
let tmpResolved = libPath.resolve(tmpPath);
|
|
473
|
-
if (!tmpResolved.startsWith(this.
|
|
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)">← Back</button>';
|
|
345
|
-
tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer — ' + this.
|
|
345
|
+
tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer — ' + 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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;
|