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
@@ -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
  }
@@ -0,0 +1,560 @@
1
+ /**
2
+ * Retold Remote -- File Operation Service
3
+ *
4
+ * Provides safe file move, directory creation, and undo capabilities.
5
+ * All paths are sanitized and resolved within the content root to
6
+ * prevent directory traversal attacks.
7
+ *
8
+ * Batch moves are recorded in Bibliograph for undo support.
9
+ *
10
+ * API:
11
+ * POST /api/files/mkdir - Create directory recursively
12
+ * POST /api/files/move - Move a single file
13
+ * POST /api/files/move-batch - Move multiple files atomically
14
+ * POST /api/files/undo-batch - Reverse a completed batch
15
+ *
16
+ * @license MIT
17
+ */
18
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
19
+ const libFs = require('fs');
20
+ const libPath = require('path');
21
+
22
+ const SOURCE_NAME = 'retold-remote-file-ops';
23
+
24
+ const _DefaultServiceConfiguration =
25
+ {
26
+ "ContentPath": ".",
27
+ "MaxBatchSize": 1000
28
+ };
29
+
30
+ class RetoldRemoteFileOperationService extends libFableServiceProviderBase
31
+ {
32
+ constructor(pFable, pOptions, pServiceHash)
33
+ {
34
+ super(pFable, pOptions, pServiceHash);
35
+
36
+ this.serviceType = 'RetoldRemoteFileOperationService';
37
+
38
+ // Merge with defaults
39
+ for (let tmpKey in _DefaultServiceConfiguration)
40
+ {
41
+ if (!(tmpKey in this.options))
42
+ {
43
+ this.options[tmpKey] = _DefaultServiceConfiguration[tmpKey];
44
+ }
45
+ }
46
+
47
+ this.contentPath = libPath.resolve(this.options.ContentPath);
48
+
49
+ this.fable.log.info('File Operation Service: initialized');
50
+ this.fable.log.info(` Content root: ${this.contentPath}`);
51
+ this.fable.log.info(` Max batch size: ${this.options.MaxBatchSize}`);
52
+ }
53
+
54
+ /**
55
+ * Sanitize a path to prevent directory traversal.
56
+ * Returns null if invalid.
57
+ *
58
+ * @param {string} pPath
59
+ * @returns {string|null}
60
+ */
61
+ _sanitizePath(pPath)
62
+ {
63
+ if (!pPath || typeof (pPath) !== 'string')
64
+ {
65
+ return null;
66
+ }
67
+
68
+ let tmpPath = decodeURIComponent(pPath).replace(/^\/+/, '');
69
+
70
+ if (tmpPath.includes('..') || libPath.isAbsolute(tmpPath))
71
+ {
72
+ return null;
73
+ }
74
+
75
+ // Resolve and verify it stays within content root
76
+ let tmpAbsPath = libPath.join(this.contentPath, tmpPath);
77
+ if (!tmpAbsPath.startsWith(this.contentPath))
78
+ {
79
+ return null;
80
+ }
81
+
82
+ return tmpPath;
83
+ }
84
+
85
+ /**
86
+ * Move a single file from source to destination within the content root.
87
+ * Creates parent directories as needed.
88
+ *
89
+ * @param {string} pSource - Relative source path
90
+ * @param {string} pDestination - Relative destination path
91
+ * @param {function} fCallback - Callback(pError, { Source, Destination })
92
+ */
93
+ moveFile(pSource, pDestination, fCallback)
94
+ {
95
+ try
96
+ {
97
+ let tmpSourceRel = this._sanitizePath(pSource);
98
+ let tmpDestRel = this._sanitizePath(pDestination);
99
+
100
+ if (!tmpSourceRel)
101
+ {
102
+ return fCallback(new Error('Invalid source path.'));
103
+ }
104
+ if (!tmpDestRel)
105
+ {
106
+ return fCallback(new Error('Invalid destination path.'));
107
+ }
108
+
109
+ let tmpSourceAbs = libPath.join(this.contentPath, tmpSourceRel);
110
+ let tmpDestAbs = libPath.join(this.contentPath, tmpDestRel);
111
+
112
+ if (!libFs.existsSync(tmpSourceAbs))
113
+ {
114
+ return fCallback(new Error('Source file not found: ' + tmpSourceRel));
115
+ }
116
+
117
+ // Don't overwrite existing files
118
+ if (libFs.existsSync(tmpDestAbs))
119
+ {
120
+ return fCallback(new Error('Destination already exists: ' + tmpDestRel));
121
+ }
122
+
123
+ // Create parent directory if needed
124
+ let tmpDestDir = libPath.dirname(tmpDestAbs);
125
+ if (!libFs.existsSync(tmpDestDir))
126
+ {
127
+ libFs.mkdirSync(tmpDestDir, { recursive: true });
128
+ }
129
+
130
+ // Try rename first (fast, same filesystem)
131
+ try
132
+ {
133
+ libFs.renameSync(tmpSourceAbs, tmpDestAbs);
134
+ }
135
+ catch (pRenameError)
136
+ {
137
+ // EXDEV = cross-device link; fall back to copy+unlink
138
+ if (pRenameError.code === 'EXDEV')
139
+ {
140
+ libFs.copyFileSync(tmpSourceAbs, tmpDestAbs);
141
+ libFs.unlinkSync(tmpSourceAbs);
142
+ }
143
+ else
144
+ {
145
+ throw pRenameError;
146
+ }
147
+ }
148
+
149
+ return fCallback(null, { Source: tmpSourceRel, Destination: tmpDestRel });
150
+ }
151
+ catch (pError)
152
+ {
153
+ return fCallback(pError);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Move multiple files. Records the batch in Bibliograph for undo.
159
+ *
160
+ * @param {Array<{Source:string, Destination:string}>} pMoves
161
+ * @param {function} fCallback - Callback(pError, { BatchGUID, Completed, Failed })
162
+ */
163
+ moveBatch(pMoves, fCallback)
164
+ {
165
+ let tmpSelf = this;
166
+
167
+ if (!Array.isArray(pMoves) || pMoves.length === 0)
168
+ {
169
+ return fCallback(new Error('No moves provided.'));
170
+ }
171
+
172
+ if (pMoves.length > this.options.MaxBatchSize)
173
+ {
174
+ return fCallback(new Error(`Batch exceeds maximum size of ${this.options.MaxBatchSize}.`));
175
+ }
176
+
177
+ // Pre-validate all paths before moving anything
178
+ let tmpValidatedMoves = [];
179
+ for (let i = 0; i < pMoves.length; i++)
180
+ {
181
+ let tmpMove = pMoves[i];
182
+ let tmpSourceRel = this._sanitizePath(tmpMove.Source);
183
+ let tmpDestRel = this._sanitizePath(tmpMove.Destination);
184
+
185
+ if (!tmpSourceRel || !tmpDestRel)
186
+ {
187
+ return fCallback(new Error(`Invalid path at index ${i}.`));
188
+ }
189
+
190
+ let tmpSourceAbs = libPath.join(this.contentPath, tmpSourceRel);
191
+ if (!libFs.existsSync(tmpSourceAbs))
192
+ {
193
+ return fCallback(new Error(`Source not found at index ${i}: ${tmpSourceRel}`));
194
+ }
195
+
196
+ let tmpDestAbs = libPath.join(this.contentPath, tmpDestRel);
197
+ if (libFs.existsSync(tmpDestAbs))
198
+ {
199
+ return fCallback(new Error(`Destination already exists at index ${i}: ${tmpDestRel}`));
200
+ }
201
+
202
+ tmpValidatedMoves.push(
203
+ {
204
+ Source: tmpSourceRel,
205
+ Destination: tmpDestRel,
206
+ SourceAbs: tmpSourceAbs,
207
+ DestAbs: tmpDestAbs
208
+ });
209
+ }
210
+
211
+ // Execute moves sequentially
212
+ let tmpCompleted = [];
213
+ let tmpFailed = [];
214
+ let tmpIndex = 0;
215
+
216
+ function _moveNext()
217
+ {
218
+ if (tmpIndex >= tmpValidatedMoves.length)
219
+ {
220
+ // Record batch in Bibliograph for undo
221
+ let tmpBatchGUID = tmpSelf.fable.getUUID();
222
+ let tmpBatchRecord =
223
+ {
224
+ GUID: tmpBatchGUID,
225
+ Timestamp: new Date().toISOString(),
226
+ Moves: tmpCompleted,
227
+ FailedCount: tmpFailed.length,
228
+ Status: (tmpFailed.length === 0) ? 'completed' : 'partial'
229
+ };
230
+
231
+ tmpSelf.fable.Bibliograph.write(SOURCE_NAME, tmpBatchGUID, tmpBatchRecord,
232
+ (pWriteError) =>
233
+ {
234
+ if (pWriteError)
235
+ {
236
+ tmpSelf.fable.log.warn(`Failed to record batch: ${pWriteError.message}`);
237
+ }
238
+
239
+ return fCallback(null,
240
+ {
241
+ BatchGUID: tmpBatchGUID,
242
+ Completed: tmpCompleted,
243
+ Failed: tmpFailed,
244
+ TotalMoved: tmpCompleted.length,
245
+ TotalFailed: tmpFailed.length
246
+ });
247
+ });
248
+ return;
249
+ }
250
+
251
+ let tmpMove = tmpValidatedMoves[tmpIndex];
252
+ tmpIndex++;
253
+
254
+ try
255
+ {
256
+ // Create parent directory if needed
257
+ let tmpDestDir = libPath.dirname(tmpMove.DestAbs);
258
+ if (!libFs.existsSync(tmpDestDir))
259
+ {
260
+ libFs.mkdirSync(tmpDestDir, { recursive: true });
261
+ }
262
+
263
+ // Try rename first
264
+ try
265
+ {
266
+ libFs.renameSync(tmpMove.SourceAbs, tmpMove.DestAbs);
267
+ }
268
+ catch (pRenameError)
269
+ {
270
+ if (pRenameError.code === 'EXDEV')
271
+ {
272
+ libFs.copyFileSync(tmpMove.SourceAbs, tmpMove.DestAbs);
273
+ libFs.unlinkSync(tmpMove.SourceAbs);
274
+ }
275
+ else
276
+ {
277
+ throw pRenameError;
278
+ }
279
+ }
280
+
281
+ tmpCompleted.push({ Source: tmpMove.Source, Destination: tmpMove.Destination });
282
+ }
283
+ catch (pMoveError)
284
+ {
285
+ tmpFailed.push(
286
+ {
287
+ Source: tmpMove.Source,
288
+ Destination: tmpMove.Destination,
289
+ Error: pMoveError.message
290
+ });
291
+ }
292
+
293
+ _moveNext();
294
+ }
295
+
296
+ // Initialize Bibliograph source, then proceed
297
+ this.fable.Bibliograph.createSource(SOURCE_NAME,
298
+ (pCreateError) =>
299
+ {
300
+ // createSource may error if source exists; that's fine
301
+ _moveNext();
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Undo a previously executed batch by reversing all moves.
307
+ *
308
+ * @param {string} pBatchGUID
309
+ * @param {function} fCallback - Callback(pError, { Reversed, Failed })
310
+ */
311
+ undoBatch(pBatchGUID, fCallback)
312
+ {
313
+ let tmpSelf = this;
314
+
315
+ this.fable.Bibliograph.createSource(SOURCE_NAME,
316
+ (pCreateError) =>
317
+ {
318
+ tmpSelf.fable.Bibliograph.read(SOURCE_NAME, pBatchGUID,
319
+ (pReadError, pRecord) =>
320
+ {
321
+ if (pReadError || !pRecord)
322
+ {
323
+ return fCallback(new Error('Batch not found: ' + pBatchGUID));
324
+ }
325
+
326
+ if (pRecord.Status === 'undone')
327
+ {
328
+ return fCallback(new Error('Batch already undone.'));
329
+ }
330
+
331
+ let tmpMoves = pRecord.Moves || [];
332
+ let tmpReversed = [];
333
+ let tmpFailed = [];
334
+
335
+ // Reverse in reverse order
336
+ for (let i = tmpMoves.length - 1; i >= 0; i--)
337
+ {
338
+ let tmpMove = tmpMoves[i];
339
+
340
+ try
341
+ {
342
+ let tmpCurrentAbs = libPath.join(tmpSelf.contentPath, tmpMove.Destination);
343
+ let tmpOriginalAbs = libPath.join(tmpSelf.contentPath, tmpMove.Source);
344
+
345
+ if (!libFs.existsSync(tmpCurrentAbs))
346
+ {
347
+ tmpFailed.push(
348
+ {
349
+ Source: tmpMove.Destination,
350
+ Destination: tmpMove.Source,
351
+ Error: 'File not found at current location'
352
+ });
353
+ continue;
354
+ }
355
+
356
+ // Re-create original directory if needed
357
+ let tmpOrigDir = libPath.dirname(tmpOriginalAbs);
358
+ if (!libFs.existsSync(tmpOrigDir))
359
+ {
360
+ libFs.mkdirSync(tmpOrigDir, { recursive: true });
361
+ }
362
+
363
+ try
364
+ {
365
+ libFs.renameSync(tmpCurrentAbs, tmpOriginalAbs);
366
+ }
367
+ catch (pRenameError)
368
+ {
369
+ if (pRenameError.code === 'EXDEV')
370
+ {
371
+ libFs.copyFileSync(tmpCurrentAbs, tmpOriginalAbs);
372
+ libFs.unlinkSync(tmpCurrentAbs);
373
+ }
374
+ else
375
+ {
376
+ throw pRenameError;
377
+ }
378
+ }
379
+
380
+ tmpReversed.push({ Source: tmpMove.Destination, Destination: tmpMove.Source });
381
+ }
382
+ catch (pUndoError)
383
+ {
384
+ tmpFailed.push(
385
+ {
386
+ Source: tmpMove.Destination,
387
+ Destination: tmpMove.Source,
388
+ Error: pUndoError.message
389
+ });
390
+ }
391
+ }
392
+
393
+ // Update batch record
394
+ pRecord.Status = 'undone';
395
+ pRecord.UndoneAt = new Date().toISOString();
396
+
397
+ tmpSelf.fable.Bibliograph.write(SOURCE_NAME, pBatchGUID, pRecord,
398
+ (pWriteError) =>
399
+ {
400
+ return fCallback(null,
401
+ {
402
+ Reversed: tmpReversed,
403
+ Failed: tmpFailed,
404
+ TotalReversed: tmpReversed.length,
405
+ TotalFailed: tmpFailed.length
406
+ });
407
+ });
408
+ });
409
+ });
410
+ }
411
+
412
+ /**
413
+ * Wire REST endpoints.
414
+ *
415
+ * @param {object} pServiceServer - The Orator service server
416
+ */
417
+ connectRoutes(pServiceServer)
418
+ {
419
+ let tmpSelf = this;
420
+ let tmpServer = pServiceServer.server;
421
+
422
+ // --- POST /api/files/mkdir ---
423
+ tmpServer.post('/api/files/mkdir',
424
+ (pRequest, pResponse, fNext) =>
425
+ {
426
+ try
427
+ {
428
+ let tmpPath = tmpSelf._sanitizePath(pRequest.body && pRequest.body.Path);
429
+
430
+ if (!tmpPath)
431
+ {
432
+ pResponse.send(400, { Success: false, Error: 'Invalid path.' });
433
+ return fNext();
434
+ }
435
+
436
+ let tmpAbsPath = libPath.join(tmpSelf.contentPath, tmpPath);
437
+
438
+ if (libFs.existsSync(tmpAbsPath))
439
+ {
440
+ pResponse.send(200, { Success: true, Path: tmpPath, AlreadyExists: true });
441
+ return fNext();
442
+ }
443
+
444
+ libFs.mkdirSync(tmpAbsPath, { recursive: true });
445
+ pResponse.send(200, { Success: true, Path: tmpPath });
446
+ return fNext();
447
+ }
448
+ catch (pError)
449
+ {
450
+ pResponse.send(500, { Success: false, Error: pError.message });
451
+ return fNext();
452
+ }
453
+ });
454
+
455
+ // --- POST /api/files/move ---
456
+ tmpServer.post('/api/files/move',
457
+ (pRequest, pResponse, fNext) =>
458
+ {
459
+ try
460
+ {
461
+ let tmpSource = pRequest.body && pRequest.body.Source;
462
+ let tmpDest = pRequest.body && pRequest.body.Destination;
463
+
464
+ tmpSelf.moveFile(tmpSource, tmpDest,
465
+ (pError, pResult) =>
466
+ {
467
+ if (pError)
468
+ {
469
+ pResponse.send(400, { Success: false, Error: pError.message });
470
+ return fNext();
471
+ }
472
+ pResponse.send(200, { Success: true, Source: pResult.Source, Destination: pResult.Destination });
473
+ return fNext();
474
+ });
475
+ }
476
+ catch (pError)
477
+ {
478
+ pResponse.send(500, { Success: false, Error: pError.message });
479
+ return fNext();
480
+ }
481
+ });
482
+
483
+ // --- POST /api/files/move-batch ---
484
+ tmpServer.post('/api/files/move-batch',
485
+ (pRequest, pResponse, fNext) =>
486
+ {
487
+ try
488
+ {
489
+ let tmpMoves = pRequest.body && pRequest.body.Moves;
490
+
491
+ tmpSelf.moveBatch(tmpMoves,
492
+ (pError, pResult) =>
493
+ {
494
+ if (pError)
495
+ {
496
+ pResponse.send(400, { Success: false, Error: pError.message });
497
+ return fNext();
498
+ }
499
+ pResponse.send(200,
500
+ {
501
+ Success: true,
502
+ BatchGUID: pResult.BatchGUID,
503
+ TotalMoved: pResult.TotalMoved,
504
+ TotalFailed: pResult.TotalFailed,
505
+ Completed: pResult.Completed,
506
+ Failed: pResult.Failed
507
+ });
508
+ return fNext();
509
+ });
510
+ }
511
+ catch (pError)
512
+ {
513
+ pResponse.send(500, { Success: false, Error: pError.message });
514
+ return fNext();
515
+ }
516
+ });
517
+
518
+ // --- POST /api/files/undo-batch ---
519
+ tmpServer.post('/api/files/undo-batch',
520
+ (pRequest, pResponse, fNext) =>
521
+ {
522
+ try
523
+ {
524
+ let tmpBatchGUID = pRequest.body && pRequest.body.BatchGUID;
525
+
526
+ if (!tmpBatchGUID)
527
+ {
528
+ pResponse.send(400, { Success: false, Error: 'Missing BatchGUID.' });
529
+ return fNext();
530
+ }
531
+
532
+ tmpSelf.undoBatch(tmpBatchGUID,
533
+ (pError, pResult) =>
534
+ {
535
+ if (pError)
536
+ {
537
+ pResponse.send(400, { Success: false, Error: pError.message });
538
+ return fNext();
539
+ }
540
+ pResponse.send(200,
541
+ {
542
+ Success: true,
543
+ TotalReversed: pResult.TotalReversed,
544
+ TotalFailed: pResult.TotalFailed,
545
+ Reversed: pResult.Reversed,
546
+ Failed: pResult.Failed
547
+ });
548
+ return fNext();
549
+ });
550
+ }
551
+ catch (pError)
552
+ {
553
+ pResponse.send(500, { Success: false, Error: pError.message });
554
+ return fNext();
555
+ }
556
+ });
557
+ }
558
+ }
559
+
560
+ module.exports = RetoldRemoteFileOperationService;
@@ -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, 'mpg': true, 'mpeg': true, 'mpe': true, 'mpv': true, 'm2v': true, 'ts': true, 'mts': true, 'm2ts': true, 'vob': true, '3gp': true, '3g2': true, 'f4v': true, 'rm': true, 'rmvb': true, 'divx': true, 'asf': true, 'mxf': true, 'dv': true, 'nsv': true, 'nuv': true, 'y4m': true, 'wtv': true, 'swf': true, 'dat': 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
  /**
@@ -265,6 +254,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
265
254
  tmpProbe.Height = pMetadata.height;
266
255
  tmpProbe.Codec = pMetadata.codec;
267
256
  tmpProbe.Bitrate = pMetadata.bitrate;
257
+ tmpProbe.Tags = pMetadata.tags || {};
268
258
  }
269
259
  pResponse.send(tmpProbe);
270
260
  return fNext();
@@ -502,6 +492,17 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
502
492
  {
503
493
  tmpResult.duration = parseFloat(tmpData.format.duration) || null;
504
494
  tmpResult.bitrate = parseInt(tmpData.format.bit_rate, 10) || null;
495
+
496
+ // Extract format-level tags (ID3, Vorbis comments, etc.)
497
+ if (tmpData.format.tags)
498
+ {
499
+ tmpResult.tags = {};
500
+ let tmpTagKeys = Object.keys(tmpData.format.tags);
501
+ for (let t = 0; t < tmpTagKeys.length; t++)
502
+ {
503
+ tmpResult.tags[tmpTagKeys[t].toLowerCase()] = tmpData.format.tags[tmpTagKeys[t]];
504
+ }
505
+ }
505
506
  }
506
507
 
507
508
  // Find video stream for dimensions