retold-remote 0.0.23 → 0.0.26
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 +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- 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 +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
const libFs = require('fs');
|
|
28
28
|
const libPath = require('path');
|
|
29
|
+
const libOs = require('os');
|
|
30
|
+
const libCrypto = require('crypto');
|
|
29
31
|
const libChildProcess = require('child_process');
|
|
30
32
|
|
|
31
33
|
const libFable = require('fable');
|
|
@@ -48,8 +50,12 @@ const libRetoldRemoteMetadataCache = require('../server/RetoldRemote-MetadataCac
|
|
|
48
50
|
const libRetoldRemoteFileOperationService = require('../server/RetoldRemote-FileOperationService.js');
|
|
49
51
|
const libRetoldRemoteAISortService = require('../server/RetoldRemote-AISortService.js');
|
|
50
52
|
const libRetoldRemoteImageService = require('../server/RetoldRemote-ImageService.js');
|
|
53
|
+
const libRetoldRemoteSubimageService = require('../server/RetoldRemote-SubimageService.js');
|
|
54
|
+
const libRetoldRemoteCollectionExportService = require('../server/RetoldRemote-CollectionExportService.js');
|
|
55
|
+
const libRetoldRemoteOperationBroadcaster = require('../server/RetoldRemote-OperationBroadcaster.js');
|
|
51
56
|
const libRetoldRemoteUltravisorDispatcher = require('../server/RetoldRemote-UltravisorDispatcher.js');
|
|
52
57
|
const libRetoldRemoteUltravisorBeacon = require('../server/RetoldRemote-UltravisorBeacon.js');
|
|
58
|
+
const libOratorConversion = require('orator-conversion');
|
|
53
59
|
const libUrl = require('url');
|
|
54
60
|
|
|
55
61
|
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
@@ -104,6 +110,28 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
104
110
|
|
|
105
111
|
let tmpFable = new libFable(tmpSettings);
|
|
106
112
|
|
|
113
|
+
// Apply LogNoisiness from RETOLD_LOG_NOISINESS env var (or pOptions override).
|
|
114
|
+
// Pict-style log noisiness is a 0-5 scale where 0 is silent (production
|
|
115
|
+
// default) and 5 shows everything. Diagnostic log statements throughout
|
|
116
|
+
// retold-remote and Ultravisor are gated with `if (this.fable.LogNoisiness
|
|
117
|
+
// >= N)` so they're free at level 0 and explosively detailed at level 4-5.
|
|
118
|
+
// Useful values:
|
|
119
|
+
// 1 — high-level decisions (auto-detected shared-fs peer X)
|
|
120
|
+
// 2 — entry points and decisions in shared-fs / dispatch paths
|
|
121
|
+
// 3 — per-candidate iteration in reachability
|
|
122
|
+
// 4 — per-mount comparison details
|
|
123
|
+
// 5 — everything
|
|
124
|
+
let tmpNoisy = parseInt(pOptions.LogNoisiness, 10);
|
|
125
|
+
if (isNaN(tmpNoisy))
|
|
126
|
+
{
|
|
127
|
+
tmpNoisy = parseInt(process.env.RETOLD_LOG_NOISINESS, 10);
|
|
128
|
+
}
|
|
129
|
+
if (!isNaN(tmpNoisy) && tmpNoisy > 0)
|
|
130
|
+
{
|
|
131
|
+
tmpFable.LogNoisiness = tmpNoisy;
|
|
132
|
+
tmpFable.log.info(`Retold-Remote: LogNoisiness=${tmpNoisy} (verbose diagnostics enabled).`);
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
// Ensure the content directory exists
|
|
108
136
|
if (!libFs.existsSync(tmpContentPath))
|
|
109
137
|
{
|
|
@@ -189,6 +217,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
189
217
|
ContentPath: tmpContentPath
|
|
190
218
|
});
|
|
191
219
|
|
|
220
|
+
// Set up the subimage region service
|
|
221
|
+
let tmpSubimageService = new libRetoldRemoteSubimageService(tmpFable,
|
|
222
|
+
{
|
|
223
|
+
ContentPath: tmpContentPath
|
|
224
|
+
});
|
|
225
|
+
|
|
192
226
|
// Set up the metadata cache service
|
|
193
227
|
let tmpMetadataCache = new libRetoldRemoteMetadataCache(tmpFable,
|
|
194
228
|
{
|
|
@@ -212,6 +246,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
212
246
|
let tmpCollectionService = new libRetoldRemoteCollectionService(tmpFable, {});
|
|
213
247
|
tmpCollectionService.setFileOperationService(tmpFileOperationService);
|
|
214
248
|
|
|
249
|
+
// Set up the collection export service
|
|
250
|
+
let tmpCollectionExportService = new libRetoldRemoteCollectionExportService(tmpFable,
|
|
251
|
+
{
|
|
252
|
+
ContentPath: tmpContentPath
|
|
253
|
+
});
|
|
254
|
+
|
|
215
255
|
// Set up the media service
|
|
216
256
|
let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
|
|
217
257
|
{
|
|
@@ -226,11 +266,38 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
226
266
|
// Set up the Ultravisor beacon for mesh registration
|
|
227
267
|
let tmpBeacon = new libRetoldRemoteUltravisorBeacon(tmpFable, {});
|
|
228
268
|
|
|
269
|
+
// Set up the Operation Broadcaster — WebSocket pub/sub for
|
|
270
|
+
// long-running operation progress + cooperative cancellation.
|
|
271
|
+
// Attached to the HTTP server AFTER startService(), below.
|
|
272
|
+
let tmpOperationBroadcaster = new libRetoldRemoteOperationBroadcaster(tmpFable, {});
|
|
273
|
+
|
|
274
|
+
// Set up orator-conversion for document format conversion
|
|
275
|
+
tmpFable.serviceManager.addServiceType('OratorFileTranslation', libOratorConversion);
|
|
276
|
+
let tmpConversionService = tmpFable.serviceManager.instantiateServiceProvider('OratorFileTranslation',
|
|
277
|
+
{
|
|
278
|
+
RoutePrefix: '/api/conversion',
|
|
279
|
+
MaxFileSize: 100 * 1024 * 1024, // 100MB for large documents
|
|
280
|
+
LogLevel: 1
|
|
281
|
+
});
|
|
282
|
+
|
|
229
283
|
// Wire the dispatcher to services that can offload processing
|
|
230
284
|
tmpMediaService.setDispatcher(tmpDispatcher);
|
|
231
285
|
tmpVideoFrameService.setDispatcher(tmpDispatcher);
|
|
232
286
|
tmpAudioWaveformService.setDispatcher(tmpDispatcher);
|
|
233
287
|
tmpEbookService.setDispatcher(tmpDispatcher);
|
|
288
|
+
|
|
289
|
+
// Wire the operation broadcaster to services that emit progress.
|
|
290
|
+
// Services must gracefully handle a null broadcaster — this keeps
|
|
291
|
+
// them runnable outside the stack.
|
|
292
|
+
tmpMediaService.setBroadcaster(tmpOperationBroadcaster);
|
|
293
|
+
tmpVideoFrameService.setBroadcaster(tmpOperationBroadcaster);
|
|
294
|
+
tmpAudioWaveformService.setBroadcaster(tmpOperationBroadcaster);
|
|
295
|
+
tmpEbookService.setBroadcaster(tmpOperationBroadcaster);
|
|
296
|
+
tmpImageService.setBroadcaster(tmpOperationBroadcaster);
|
|
297
|
+
tmpCollectionExportService.setBroadcaster(tmpOperationBroadcaster);
|
|
298
|
+
|
|
299
|
+
// Share the orator-conversion service with the ebook service for PDF conversion
|
|
300
|
+
tmpEbookService.setConversionService(tmpConversionService);
|
|
234
301
|
tmpImageService.setDispatcher(tmpDispatcher);
|
|
235
302
|
|
|
236
303
|
// Share tool capabilities with the image service so it can
|
|
@@ -238,10 +305,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
238
305
|
// Also provides the centrally-verified sharp module reference.
|
|
239
306
|
tmpImageService.setCapabilities(tmpMediaService.capabilities);
|
|
240
307
|
|
|
241
|
-
// Share the verified sharp module with the metadata cache
|
|
308
|
+
// Share the verified sharp module with the metadata cache, subimage, and export services
|
|
242
309
|
if (tmpMediaService.capabilities.sharpModule)
|
|
243
310
|
{
|
|
244
311
|
tmpMetadataCache.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
312
|
+
tmpSubimageService.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
313
|
+
tmpCollectionExportService.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
245
314
|
}
|
|
246
315
|
|
|
247
316
|
tmpOrator.initialize(
|
|
@@ -252,6 +321,20 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
252
321
|
// Enable body parsing
|
|
253
322
|
tmpServiceServer.server.use(tmpServiceServer.bodyParser());
|
|
254
323
|
|
|
324
|
+
// X-Op-Id extraction middleware: stash the client-provided
|
|
325
|
+
// operation id on the request so route handlers can pass it
|
|
326
|
+
// into services (for progress emission + cancellation).
|
|
327
|
+
tmpServiceServer.server.use(
|
|
328
|
+
(pRequest, pResponse, fNext) =>
|
|
329
|
+
{
|
|
330
|
+
let tmpOpId = pRequest.headers && pRequest.headers['x-op-id'];
|
|
331
|
+
if (tmpOpId && typeof tmpOpId === 'string' && tmpOpId.length <= 128)
|
|
332
|
+
{
|
|
333
|
+
pRequest.OperationId = tmpOpId;
|
|
334
|
+
}
|
|
335
|
+
return fNext();
|
|
336
|
+
});
|
|
337
|
+
|
|
255
338
|
// Hash resolution middleware: if hashed filenames is enabled,
|
|
256
339
|
// intercept ?path= query params and resolve hashes to paths.
|
|
257
340
|
// Uses three-tier lookup (memory → Bibliograph → directory walk).
|
|
@@ -470,6 +553,74 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
470
553
|
// Connect collection service API routes
|
|
471
554
|
tmpCollectionService.connectRoutes(tmpServiceServer);
|
|
472
555
|
|
|
556
|
+
// Connect subimage region service API routes
|
|
557
|
+
tmpSubimageService.connectRoutes(tmpServiceServer);
|
|
558
|
+
|
|
559
|
+
// Connect collection export service API routes
|
|
560
|
+
tmpCollectionExportService.connectRoutes(tmpServiceServer);
|
|
561
|
+
|
|
562
|
+
// Connect orator-conversion routes and register custom doc-to-pdf converter
|
|
563
|
+
if (tmpMediaService.capabilities.libreoffice)
|
|
564
|
+
{
|
|
565
|
+
let tmpSofficePath = 'soffice';
|
|
566
|
+
if (libFs.existsSync('/Applications/LibreOffice.app/Contents/MacOS/soffice'))
|
|
567
|
+
{
|
|
568
|
+
tmpSofficePath = '/Applications/LibreOffice.app/Contents/MacOS/soffice';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
tmpConversionService.addConverter('doc-to-pdf',
|
|
572
|
+
(pInputBuffer, pRequest, fCallback) =>
|
|
573
|
+
{
|
|
574
|
+
// Write input to temp file, convert with LibreOffice, return PDF buffer
|
|
575
|
+
let tmpTempDir = require('os').tmpdir();
|
|
576
|
+
let tmpUniqueId = Date.now() + '_' + Math.random().toString(36).slice(2);
|
|
577
|
+
let tmpInputExt = (pRequest.query && pRequest.query.ext) || 'docx';
|
|
578
|
+
let tmpInputPath = libPath.join(tmpTempDir, 'retold_doc_' + tmpUniqueId + '.' + tmpInputExt);
|
|
579
|
+
let tmpOutputDir = libPath.join(tmpTempDir, 'retold_docout_' + tmpUniqueId);
|
|
580
|
+
|
|
581
|
+
libFs.mkdirSync(tmpOutputDir, { recursive: true });
|
|
582
|
+
|
|
583
|
+
try
|
|
584
|
+
{
|
|
585
|
+
libFs.writeFileSync(tmpInputPath, pInputBuffer);
|
|
586
|
+
libChildProcess.execSync(
|
|
587
|
+
'"' + tmpSofficePath + '" --headless --convert-to pdf --outdir "' + tmpOutputDir + '" "' + tmpInputPath + '"',
|
|
588
|
+
{ stdio: 'ignore', timeout: 120000 });
|
|
589
|
+
|
|
590
|
+
// Find the output PDF (LibreOffice names it after the input)
|
|
591
|
+
let tmpBaseName = libPath.basename(tmpInputPath, '.' + tmpInputExt);
|
|
592
|
+
let tmpOutputPath = libPath.join(tmpOutputDir, tmpBaseName + '.pdf');
|
|
593
|
+
|
|
594
|
+
if (!libFs.existsSync(tmpOutputPath))
|
|
595
|
+
{
|
|
596
|
+
// Clean up
|
|
597
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
598
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
599
|
+
return fCallback(new Error('LibreOffice conversion produced no output.'));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let tmpPdfBuffer = libFs.readFileSync(tmpOutputPath);
|
|
603
|
+
|
|
604
|
+
// Clean up temp files
|
|
605
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
606
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
607
|
+
|
|
608
|
+
return fCallback(null, tmpPdfBuffer, 'application/pdf');
|
|
609
|
+
}
|
|
610
|
+
catch (pError)
|
|
611
|
+
{
|
|
612
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
613
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
614
|
+
return fCallback(new Error('Document conversion failed: ' + pError.message));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
tmpFable.log.info('Orator-Conversion: doc-to-pdf converter registered (LibreOffice)');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
tmpConversionService.connectRoutes();
|
|
622
|
+
tmpFable.log.info('Orator-Conversion: routes connected at /api/conversion/1.0/');
|
|
623
|
+
|
|
473
624
|
// Connect AI sort service API routes
|
|
474
625
|
tmpAISortService.connectRoutes(tmpServiceServer);
|
|
475
626
|
|
|
@@ -486,6 +637,10 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
486
637
|
{
|
|
487
638
|
tmpFable.log.info('Audio explorer state Bibliograph source initialized.');
|
|
488
639
|
});
|
|
640
|
+
tmpSubimageService.initializeState(() =>
|
|
641
|
+
{
|
|
642
|
+
tmpFable.log.info('Subimage region state Bibliograph source initialized.');
|
|
643
|
+
});
|
|
489
644
|
tmpEpubMetadataService.initialize(() =>
|
|
490
645
|
{
|
|
491
646
|
tmpFable.log.info('EPUB metadata service initialized.');
|
|
@@ -495,6 +650,112 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
495
650
|
// Non-fatal — if Ultravisor is down, processing stays local
|
|
496
651
|
});
|
|
497
652
|
|
|
653
|
+
// --- GET /api/media/pdf-text ---
|
|
654
|
+
// Extract text from a specific PDF page (or all pages if no page specified).
|
|
655
|
+
tmpServiceServer.get('/api/media/pdf-text',
|
|
656
|
+
(pRequest, pResponse, fNext) =>
|
|
657
|
+
{
|
|
658
|
+
try
|
|
659
|
+
{
|
|
660
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
661
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
662
|
+
let tmpRelPath = tmpQuery.path;
|
|
663
|
+
let tmpPageNum = parseInt(tmpQuery.page, 10) || 0;
|
|
664
|
+
|
|
665
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
666
|
+
{
|
|
667
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
668
|
+
return fNext();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
672
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
673
|
+
{
|
|
674
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
675
|
+
return fNext();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
679
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
680
|
+
{
|
|
681
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
682
|
+
return fNext();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let tmpExt = tmpRelPath.replace(/^.*\./, '').toLowerCase();
|
|
686
|
+
if (tmpExt !== 'pdf')
|
|
687
|
+
{
|
|
688
|
+
pResponse.send(400, { Success: false, Error: 'Not a PDF file.' });
|
|
689
|
+
return fNext();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// pdf-parse v2.x exports a PDFParse class with async methods.
|
|
693
|
+
// (v1.x used to export a default async function — that API is gone.)
|
|
694
|
+
let tmpPdfParseModule = require('pdf-parse');
|
|
695
|
+
let tmpPDFParseClass = tmpPdfParseModule.PDFParse;
|
|
696
|
+
if (typeof tmpPDFParseClass !== 'function')
|
|
697
|
+
{
|
|
698
|
+
pResponse.send(500, { Success: false, Error: 'pdf-parse module does not export PDFParse class. Update pdf-parse.' });
|
|
699
|
+
return fNext();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
let tmpBuffer = libFs.readFileSync(tmpAbsPath);
|
|
703
|
+
let tmpParser = new tmpPDFParseClass({ data: tmpBuffer });
|
|
704
|
+
|
|
705
|
+
let tmpExtractText;
|
|
706
|
+
if (tmpPageNum > 0)
|
|
707
|
+
{
|
|
708
|
+
// Single page text — first/last bound the range
|
|
709
|
+
tmpExtractText = tmpParser.getText({ first: tmpPageNum, last: tmpPageNum });
|
|
710
|
+
}
|
|
711
|
+
else
|
|
712
|
+
{
|
|
713
|
+
// All pages
|
|
714
|
+
tmpExtractText = tmpParser.getText();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
tmpExtractText
|
|
718
|
+
.then((pData) =>
|
|
719
|
+
{
|
|
720
|
+
// pdf-parse v2 result shape: { text, total, pages: [...] } or similar
|
|
721
|
+
let tmpText = (pData && typeof pData.text === 'string') ? pData.text : '';
|
|
722
|
+
let tmpPageCount = (pData && typeof pData.total === 'number') ? pData.total
|
|
723
|
+
: (pData && Array.isArray(pData.pages)) ? pData.pages.length : 0;
|
|
724
|
+
|
|
725
|
+
pResponse.send(
|
|
726
|
+
{
|
|
727
|
+
Success: true,
|
|
728
|
+
Path: tmpRelPath,
|
|
729
|
+
PageCount: tmpPageCount,
|
|
730
|
+
Text: tmpText,
|
|
731
|
+
RequestedPage: tmpPageNum || null
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Release native resources
|
|
735
|
+
if (typeof tmpParser.destroy === 'function')
|
|
736
|
+
{
|
|
737
|
+
tmpParser.destroy().catch(() => { /* ignore */ });
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return fNext();
|
|
741
|
+
})
|
|
742
|
+
.catch((pPdfError) =>
|
|
743
|
+
{
|
|
744
|
+
if (typeof tmpParser.destroy === 'function')
|
|
745
|
+
{
|
|
746
|
+
tmpParser.destroy().catch(() => { /* ignore */ });
|
|
747
|
+
}
|
|
748
|
+
pResponse.send(500, { Success: false, Error: 'PDF parse failed: ' + pPdfError.message });
|
|
749
|
+
return fNext();
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
catch (pError)
|
|
753
|
+
{
|
|
754
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
755
|
+
return fNext();
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
498
759
|
// --- GET /api/media/metadata ---
|
|
499
760
|
// Get cached metadata (with ID3/format tags) for a single file.
|
|
500
761
|
tmpServiceServer.get('/api/media/metadata',
|
|
@@ -696,7 +957,8 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
696
957
|
count: tmpQuery.count,
|
|
697
958
|
width: tmpQuery.width,
|
|
698
959
|
height: tmpQuery.height,
|
|
699
|
-
format: tmpQuery.format
|
|
960
|
+
format: tmpQuery.format,
|
|
961
|
+
OperationId: pRequest.OperationId || null
|
|
700
962
|
},
|
|
701
963
|
(pError, pResult) =>
|
|
702
964
|
{
|
|
@@ -1307,9 +1569,17 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1307
1569
|
|
|
1308
1570
|
let tmpStat = libFs.statSync(tmpEbookPath);
|
|
1309
1571
|
|
|
1572
|
+
// Determine content type based on file extension
|
|
1573
|
+
let tmpEbookExt = tmpFilename.replace(/^.*\./, '').toLowerCase();
|
|
1574
|
+
let tmpContentType = 'application/epub+zip';
|
|
1575
|
+
if (tmpEbookExt === 'pdf')
|
|
1576
|
+
{
|
|
1577
|
+
tmpContentType = 'application/pdf';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1310
1580
|
pResponse.writeHead(200,
|
|
1311
1581
|
{
|
|
1312
|
-
'Content-Type':
|
|
1582
|
+
'Content-Type': tmpContentType,
|
|
1313
1583
|
'Content-Length': tmpStat.size,
|
|
1314
1584
|
'Cache-Control': 'public, max-age=86400'
|
|
1315
1585
|
});
|
|
@@ -1330,6 +1600,57 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1330
1600
|
}
|
|
1331
1601
|
});
|
|
1332
1602
|
|
|
1603
|
+
// --- GET /api/media/doc-convert ---
|
|
1604
|
+
// Convert a document (DOC, DOCX, RTF, ODT, WPD, etc.) to PDF.
|
|
1605
|
+
tmpServiceServer.get('/api/media/doc-convert',
|
|
1606
|
+
(pRequest, pResponse, fNext) =>
|
|
1607
|
+
{
|
|
1608
|
+
try
|
|
1609
|
+
{
|
|
1610
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
1611
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
1612
|
+
let tmpRelPath = tmpQuery.path;
|
|
1613
|
+
|
|
1614
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
1615
|
+
{
|
|
1616
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
1617
|
+
return fNext();
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
1621
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
1622
|
+
{
|
|
1623
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
1624
|
+
return fNext();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
1628
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
1629
|
+
{
|
|
1630
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
1631
|
+
return fNext();
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
tmpEbookService.convertToPdf(tmpAbsPath, tmpRelPath,
|
|
1635
|
+
(pError, pResult) =>
|
|
1636
|
+
{
|
|
1637
|
+
if (pError)
|
|
1638
|
+
{
|
|
1639
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
1640
|
+
return fNext();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
pResponse.send(pResult);
|
|
1644
|
+
return fNext();
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
catch (pError)
|
|
1648
|
+
{
|
|
1649
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
1650
|
+
return fNext();
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1333
1654
|
// --- GET /api/media/ebook-metadata ---
|
|
1334
1655
|
// Extract and return cached EPUB metadata (TOC, spine, word counts, etc.)
|
|
1335
1656
|
tmpServiceServer.get('/api/media/ebook-metadata',
|
|
@@ -1720,6 +2041,7 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1720
2041
|
}
|
|
1721
2042
|
|
|
1722
2043
|
tmpImageService.generateDziTiles(tmpAbsPath, tmpRelPath,
|
|
2044
|
+
{ OperationId: pRequest.OperationId || null },
|
|
1723
2045
|
(pError, pResult) =>
|
|
1724
2046
|
{
|
|
1725
2047
|
if (pError)
|
|
@@ -1775,6 +2097,18 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1775
2097
|
|
|
1776
2098
|
// --- GET /api/media/dzi-tile/:cacheKey/:level/:tile ---
|
|
1777
2099
|
// Serve an individual DZI tile image.
|
|
2100
|
+
//
|
|
2101
|
+
// OpenSeadragon's DziTileSource.configure() rewrites whatever Url
|
|
2102
|
+
// we hand it and appends a "_files" suffix to the last path segment
|
|
2103
|
+
// (that's how the standard DZI directory layout works — a foo.dzi
|
|
2104
|
+
// descriptor expects the tiles in a sibling foo_files/ directory).
|
|
2105
|
+
// So when we hand OSD `Url: '/api/media/dzi-tile/{cacheKey}/'` it
|
|
2106
|
+
// rewrites the tilesUrl to `/api/media/dzi-tile/{cacheKey}_files/`
|
|
2107
|
+
// and asks for tiles at `/api/media/dzi-tile/{cacheKey}_files/{level}/{x}_{y}.jpg`.
|
|
2108
|
+
//
|
|
2109
|
+
// Strip the trailing "_files" from the cacheKey before resolving so
|
|
2110
|
+
// both the canonical URL (used by anything that bypasses OSD) and
|
|
2111
|
+
// the OSD-generated URL hit the same on-disk cache directory.
|
|
1778
2112
|
tmpServiceServer.get('/api/media/dzi-tile/:cacheKey/:level/:tile',
|
|
1779
2113
|
(pRequest, pResponse, fNext) =>
|
|
1780
2114
|
{
|
|
@@ -1783,6 +2117,10 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1783
2117
|
let tmpCacheKey = pRequest.params.cacheKey;
|
|
1784
2118
|
let tmpLevel = pRequest.params.level;
|
|
1785
2119
|
let tmpTile = pRequest.params.tile;
|
|
2120
|
+
if (typeof tmpCacheKey === 'string' && tmpCacheKey.endsWith('_files'))
|
|
2121
|
+
{
|
|
2122
|
+
tmpCacheKey = tmpCacheKey.slice(0, -6);
|
|
2123
|
+
}
|
|
1786
2124
|
let tmpPath = tmpImageService.getDziTilePath(tmpCacheKey, tmpLevel, tmpTile);
|
|
1787
2125
|
|
|
1788
2126
|
if (!tmpPath)
|
|
@@ -2138,6 +2476,29 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2138
2476
|
tmpOrator.startService(
|
|
2139
2477
|
function ()
|
|
2140
2478
|
{
|
|
2479
|
+
// Attach the operation broadcaster to the underlying Node
|
|
2480
|
+
// http.Server so WebSocket upgrades on /ws/operations reach
|
|
2481
|
+
// our pub/sub hub. The Restify server exposes the raw
|
|
2482
|
+
// http.Server as `.server`.
|
|
2483
|
+
try
|
|
2484
|
+
{
|
|
2485
|
+
let tmpHttpServer = tmpOrator.serviceServer && tmpOrator.serviceServer.server
|
|
2486
|
+
? tmpOrator.serviceServer.server.server
|
|
2487
|
+
: null;
|
|
2488
|
+
if (tmpHttpServer)
|
|
2489
|
+
{
|
|
2490
|
+
tmpOperationBroadcaster.attachTo(tmpHttpServer);
|
|
2491
|
+
}
|
|
2492
|
+
else
|
|
2493
|
+
{
|
|
2494
|
+
tmpFable.log.warn('OperationBroadcaster: could not find underlying http.Server; WebSocket status unavailable');
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
catch (pAttachError)
|
|
2498
|
+
{
|
|
2499
|
+
tmpFable.log.warn('OperationBroadcaster attach failed: ' + pAttachError.message);
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2141
2502
|
let tmpServerInfo =
|
|
2142
2503
|
{
|
|
2143
2504
|
Fable: tmpFable,
|
|
@@ -2146,6 +2507,10 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2146
2507
|
ArchiveService: tmpArchiveService,
|
|
2147
2508
|
VideoFrameService: tmpVideoFrameService,
|
|
2148
2509
|
AudioWaveformService: tmpAudioWaveformService,
|
|
2510
|
+
SubimageService: tmpSubimageService,
|
|
2511
|
+
CollectionExportService: tmpCollectionExportService,
|
|
2512
|
+
ConversionService: tmpConversionService,
|
|
2513
|
+
OperationBroadcaster: tmpOperationBroadcaster,
|
|
2149
2514
|
PathRegistry: tmpPathRegistry,
|
|
2150
2515
|
ParimeCache: tmpParimeCache,
|
|
2151
2516
|
MetadataCache: tmpMetadataCache,
|
|
@@ -2160,7 +2525,7 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2160
2525
|
{
|
|
2161
2526
|
// Discover bind addresses from network interfaces
|
|
2162
2527
|
let tmpBindAddresses = [];
|
|
2163
|
-
let tmpNetworkInterfaces =
|
|
2528
|
+
let tmpNetworkInterfaces = libOs.networkInterfaces();
|
|
2164
2529
|
for (let tmpIfName of Object.keys(tmpNetworkInterfaces))
|
|
2165
2530
|
{
|
|
2166
2531
|
for (let tmpIf of tmpNetworkInterfaces[tmpIfName])
|
|
@@ -2174,6 +2539,48 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2174
2539
|
// Always include loopback as a fallback
|
|
2175
2540
|
tmpBindAddresses.push({ IP: '127.0.0.1', Port: tmpPort, Protocol: 'http' });
|
|
2176
2541
|
|
|
2542
|
+
// Shared-fs identity: when retold-remote and the in-process
|
|
2543
|
+
// orator-conversion beacon both live in the same container/host,
|
|
2544
|
+
// they share the same content mount. By computing one HostID and
|
|
2545
|
+
// one MountID here and passing the SAME values to both beacons,
|
|
2546
|
+
// the Ultravisor reachability matrix can detect the overlap and
|
|
2547
|
+
// pick the 'shared-fs' strategy instead of forcing a 374 MB
|
|
2548
|
+
// HTTP file-transfer between two beacons in the same process.
|
|
2549
|
+
//
|
|
2550
|
+
// The RETOLD_SHARED_FS_ENABLED env var provides a test hook to
|
|
2551
|
+
// disable the shared-fs advertisement. When set to 'false' or '0'
|
|
2552
|
+
// both beacons skip SharedMounts, which forces the reachability
|
|
2553
|
+
// matrix to fall through to 'direct' (HTTP file-transfer) or
|
|
2554
|
+
// 'proxy'. This lets the docker smoke test exercise the legacy
|
|
2555
|
+
// path to confirm nothing has regressed there.
|
|
2556
|
+
let tmpHostID = libOs.hostname();
|
|
2557
|
+
let tmpSharedMounts = [];
|
|
2558
|
+
let tmpSharedFsEnvValue = (typeof process.env.RETOLD_SHARED_FS_ENABLED === 'string')
|
|
2559
|
+
? process.env.RETOLD_SHARED_FS_ENABLED.trim().toLowerCase()
|
|
2560
|
+
: '';
|
|
2561
|
+
let tmpSharedFsEnabled = !(tmpSharedFsEnvValue === 'false' || tmpSharedFsEnvValue === '0' || tmpSharedFsEnvValue === 'no' || tmpSharedFsEnvValue === 'off');
|
|
2562
|
+
if (!tmpSharedFsEnabled)
|
|
2563
|
+
{
|
|
2564
|
+
tmpFable.log.warn(`Ultravisor Beacons: RETOLD_SHARED_FS_ENABLED=${process.env.RETOLD_SHARED_FS_ENABLED} -- SharedMounts advertisement DISABLED (forcing direct/proxy transfer strategy).`);
|
|
2565
|
+
}
|
|
2566
|
+
else
|
|
2567
|
+
{
|
|
2568
|
+
try
|
|
2569
|
+
{
|
|
2570
|
+
let tmpMountRoot = libPath.resolve(tmpContentPath);
|
|
2571
|
+
let tmpStat = libFs.statSync(tmpMountRoot);
|
|
2572
|
+
let tmpMountID = libCrypto.createHash('sha256')
|
|
2573
|
+
.update(tmpStat.dev + ':' + tmpMountRoot)
|
|
2574
|
+
.digest('hex').substring(0, 16);
|
|
2575
|
+
tmpSharedMounts.push({ MountID: tmpMountID, Root: tmpMountRoot });
|
|
2576
|
+
tmpFable.log.info(`Ultravisor Beacons: advertising shared mount [${tmpMountID}] = ${tmpMountRoot} (HostID: ${tmpHostID})`);
|
|
2577
|
+
}
|
|
2578
|
+
catch (pMountError)
|
|
2579
|
+
{
|
|
2580
|
+
tmpFable.log.warn(`Ultravisor Beacons: could not stat content path for shared-fs detection: ${pMountError.message}`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2177
2584
|
tmpBeacon.connectBeacon(
|
|
2178
2585
|
{
|
|
2179
2586
|
ServerURL: pOptions.UltravisorURL,
|
|
@@ -2182,7 +2589,9 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2182
2589
|
ContentBaseURL: '/content/',
|
|
2183
2590
|
CacheRoot: tmpCacheRoot,
|
|
2184
2591
|
StagingPath: tmpCacheRoot || process.cwd(),
|
|
2185
|
-
BindAddresses: tmpBindAddresses
|
|
2592
|
+
BindAddresses: tmpBindAddresses,
|
|
2593
|
+
HostID: tmpHostID,
|
|
2594
|
+
SharedMounts: tmpSharedMounts
|
|
2186
2595
|
},
|
|
2187
2596
|
(pBeaconError) =>
|
|
2188
2597
|
{
|
|
@@ -2191,8 +2600,52 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2191
2600
|
tmpFable.log.warn(`Ultravisor Beacon: registration failed (server may not be running): ${pBeaconError.message}`);
|
|
2192
2601
|
tmpFable.log.warn('Ultravisor Beacon: server is still running. Beacon will not be active.');
|
|
2193
2602
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2603
|
+
|
|
2604
|
+
// Also register orator-conversion as a separate beacon
|
|
2605
|
+
// so its MediaConversion capability (ImageResize, VideoExtractFrame,
|
|
2606
|
+
// etc.) becomes available as auto-generated task types in the
|
|
2607
|
+
// coordinator's catalog. Without this, operations like
|
|
2608
|
+
// rr-image-thumbnail can't find their `beacon-mediaconversion-imageresize`
|
|
2609
|
+
// process node and silently fall back to local processing.
|
|
2610
|
+
//
|
|
2611
|
+
// We pass the SAME HostID and SharedMounts as the retold-remote
|
|
2612
|
+
// beacon above so the reachability matrix knows these two beacons
|
|
2613
|
+
// can read each other's files directly through the shared mount.
|
|
2614
|
+
try
|
|
2615
|
+
{
|
|
2616
|
+
let tmpConvStagingPath = libPath.join(tmpCacheRoot || process.cwd(), 'orator-conversion-staging');
|
|
2617
|
+
tmpConversionService.connectBeacon(
|
|
2618
|
+
{
|
|
2619
|
+
ServerURL: pOptions.UltravisorURL,
|
|
2620
|
+
Name: 'orator-conversion',
|
|
2621
|
+
MaxConcurrent: 2,
|
|
2622
|
+
StagingPath: tmpConvStagingPath,
|
|
2623
|
+
BindAddresses: tmpBindAddresses,
|
|
2624
|
+
FfmpegPath: 'ffmpeg',
|
|
2625
|
+
FfprobePath: 'ffprobe',
|
|
2626
|
+
HostID: tmpHostID,
|
|
2627
|
+
SharedMounts: tmpSharedMounts
|
|
2628
|
+
},
|
|
2629
|
+
(pConvBeaconError) =>
|
|
2630
|
+
{
|
|
2631
|
+
if (pConvBeaconError)
|
|
2632
|
+
{
|
|
2633
|
+
tmpFable.log.warn('Orator-Conversion beacon registration failed: ' + pConvBeaconError.message);
|
|
2634
|
+
tmpFable.log.warn('Orator-Conversion: media conversion task types will not be available; operations will fall back to local processing.');
|
|
2635
|
+
}
|
|
2636
|
+
else
|
|
2637
|
+
{
|
|
2638
|
+
tmpFable.log.info('Orator-Conversion beacon connected — MediaConversion capability registered with coordinator.');
|
|
2639
|
+
}
|
|
2640
|
+
// Non-fatal — return server info regardless
|
|
2641
|
+
return fCallback(null, tmpServerInfo);
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
catch (pConvSetupError)
|
|
2645
|
+
{
|
|
2646
|
+
tmpFable.log.warn('Orator-Conversion beacon setup failed: ' + pConvSetupError.message);
|
|
2647
|
+
return fCallback(null, tmpServerInfo);
|
|
2648
|
+
}
|
|
2196
2649
|
});
|
|
2197
2650
|
}
|
|
2198
2651
|
else
|