retold-remote 0.0.1 → 0.0.2
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/html/index.html +2 -0
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +46 -5
- package/source/cli/RetoldRemote-CLI-Run.js +0 -0
- package/source/cli/RetoldRemote-Server-Setup.js +790 -8
- package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
- package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
- package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
- package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
- package/source/server/RetoldRemote-ArchiveService.js +830 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
- package/source/server/RetoldRemote-EbookService.js +242 -0
- package/source/server/RetoldRemote-MediaService.js +1 -1
- package/source/server/RetoldRemote-ToolDetector.js +31 -1
- package/source/server/RetoldRemote-VideoFrameService.js +486 -0
- package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
- package/source/views/PictView-Remote-Gallery.js +141 -2
- package/source/views/PictView-Remote-Layout.js +18 -27
- package/source/views/PictView-Remote-MediaViewer.js +638 -39
- package/source/views/PictView-Remote-SettingsPanel.js +23 -0
- package/source/views/PictView-Remote-TopBar.js +121 -0
- package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
- package/web-application/index.html +2 -0
- package/web-application/js/epub.min.js +1 -0
- package/web-application/retold-remote.js +7030 -1244
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -44
- package/web-application/retold-remote.min.js.map +1 -1
- package/web-application/retold-remote.compatible.js +0 -5764
- package/web-application/retold-remote.compatible.js.map +0 -1
- package/web-application/retold-remote.compatible.min.js +0 -120
- package/web-application/retold-remote.compatible.min.js.map +0 -1
|
@@ -12,16 +12,21 @@
|
|
|
12
12
|
* specific endpoints (save, upload) that aren't needed here.
|
|
13
13
|
*
|
|
14
14
|
* @param {object} pOptions
|
|
15
|
-
* @param {string} pOptions.ContentPath
|
|
16
|
-
* @param {string} pOptions.DistPath
|
|
17
|
-
* @param {number} pOptions.Port
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
15
|
+
* @param {string} pOptions.ContentPath - Absolute path to the media folder to browse
|
|
16
|
+
* @param {string} pOptions.DistPath - Absolute path to the built web-application folder
|
|
17
|
+
* @param {number} pOptions.Port - HTTP port
|
|
18
|
+
* @param {boolean} [pOptions.HashedFilenames] - Enable hashed filenames mode
|
|
19
|
+
* @param {string} [pOptions.CacheRoot] - Root cache directory (default: ./dist/retold-cache/)
|
|
20
|
+
* @param {string} [pOptions.CacheThumbnails] - Override thumbnails cache directory
|
|
21
|
+
* @param {string} [pOptions.CacheArchives] - Override archives cache directory
|
|
22
|
+
* @param {string} [pOptions.CacheVideoFrames] - Override video-frames cache directory
|
|
23
|
+
* @param {string} [pOptions.CacheAudioWaveforms] - Override audio-waveforms cache directory
|
|
24
|
+
* @param {Function} fCallback - Callback(pError, { Fable, Orator, Port })
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
const libFs = require('fs');
|
|
24
28
|
const libPath = require('path');
|
|
29
|
+
const libChildProcess = require('child_process');
|
|
25
30
|
|
|
26
31
|
const libFable = require('fable');
|
|
27
32
|
const libOrator = require('orator');
|
|
@@ -30,6 +35,11 @@ const libFileBrowserService = require('pict-section-filebrowser').FileBrowserSer
|
|
|
30
35
|
|
|
31
36
|
const libRetoldRemoteMediaService = require('../server/RetoldRemote-MediaService.js');
|
|
32
37
|
const libRetoldRemotePathRegistry = require('../server/RetoldRemote-PathRegistry.js');
|
|
38
|
+
const libRetoldRemoteArchiveService = require('../server/RetoldRemote-ArchiveService.js');
|
|
39
|
+
const libRetoldRemoteVideoFrameService = require('../server/RetoldRemote-VideoFrameService.js');
|
|
40
|
+
const libRetoldRemoteAudioWaveformService = require('../server/RetoldRemote-AudioWaveformService.js');
|
|
41
|
+
const libRetoldRemoteEbookService = require('../server/RetoldRemote-EbookService.js');
|
|
42
|
+
const libUrl = require('url');
|
|
33
43
|
|
|
34
44
|
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
35
45
|
{
|
|
@@ -79,11 +89,63 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
79
89
|
tmpFable.log.info('Hashed filenames mode: ENABLED');
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
// --- Resolve cache paths ---
|
|
93
|
+
// Default root: ./dist/retold-cache/ relative to process cwd.
|
|
94
|
+
// Individual overrides take precedence over root.
|
|
95
|
+
let tmpCacheRoot = pOptions.CacheRoot
|
|
96
|
+
|| libPath.resolve(process.cwd(), 'dist', 'retold-cache');
|
|
97
|
+
|
|
98
|
+
let tmpCacheThumbnails = pOptions.CacheThumbnails
|
|
99
|
+
|| libPath.join(tmpCacheRoot, 'thumbnails');
|
|
100
|
+
let tmpCacheArchives = pOptions.CacheArchives
|
|
101
|
+
|| libPath.join(tmpCacheRoot, 'archives');
|
|
102
|
+
let tmpCacheVideoFrames = pOptions.CacheVideoFrames
|
|
103
|
+
|| libPath.join(tmpCacheRoot, 'video-frames');
|
|
104
|
+
let tmpCacheAudioWaveforms = pOptions.CacheAudioWaveforms
|
|
105
|
+
|| libPath.join(tmpCacheRoot, 'audio-waveforms');
|
|
106
|
+
let tmpCacheEbooks = pOptions.CacheEbooks
|
|
107
|
+
|| libPath.join(tmpCacheRoot, 'ebook-conversions');
|
|
108
|
+
|
|
109
|
+
tmpFable.log.info(`Cache root: ${tmpCacheRoot}`);
|
|
110
|
+
tmpFable.log.info(` Thumbnails: ${tmpCacheThumbnails}`);
|
|
111
|
+
tmpFable.log.info(` Archives: ${tmpCacheArchives}`);
|
|
112
|
+
tmpFable.log.info(` Video frames: ${tmpCacheVideoFrames}`);
|
|
113
|
+
tmpFable.log.info(` Audio waveforms: ${tmpCacheAudioWaveforms}`);
|
|
114
|
+
tmpFable.log.info(` Ebook conversions: ${tmpCacheEbooks}`);
|
|
115
|
+
|
|
116
|
+
// Set up the archive service
|
|
117
|
+
let tmpArchiveService = new libRetoldRemoteArchiveService(tmpFable,
|
|
118
|
+
{
|
|
119
|
+
ContentPath: tmpContentPath,
|
|
120
|
+
CachePath: tmpCacheArchives
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Set up the video frame service
|
|
124
|
+
let tmpVideoFrameService = new libRetoldRemoteVideoFrameService(tmpFable,
|
|
125
|
+
{
|
|
126
|
+
ContentPath: tmpContentPath,
|
|
127
|
+
CachePath: tmpCacheVideoFrames
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Set up the audio waveform service
|
|
131
|
+
let tmpAudioWaveformService = new libRetoldRemoteAudioWaveformService(tmpFable,
|
|
132
|
+
{
|
|
133
|
+
ContentPath: tmpContentPath,
|
|
134
|
+
CachePath: tmpCacheAudioWaveforms
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Set up the ebook conversion service
|
|
138
|
+
let tmpEbookService = new libRetoldRemoteEbookService(tmpFable,
|
|
139
|
+
{
|
|
140
|
+
ContentPath: tmpContentPath,
|
|
141
|
+
CachePath: tmpCacheEbooks
|
|
142
|
+
});
|
|
143
|
+
|
|
82
144
|
// Set up the media service
|
|
83
145
|
let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
|
|
84
146
|
{
|
|
85
147
|
ContentPath: tmpContentPath,
|
|
86
|
-
ThumbnailCachePath:
|
|
148
|
+
ThumbnailCachePath: tmpCacheThumbnails,
|
|
87
149
|
APIRoutePrefix: '/api/media',
|
|
88
150
|
PathRegistry: tmpPathRegistry
|
|
89
151
|
});
|
|
@@ -133,7 +195,22 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
133
195
|
pResponse.send(
|
|
134
196
|
{
|
|
135
197
|
Success: true,
|
|
136
|
-
HashedFilenames: tmpHashedFilenames
|
|
198
|
+
HashedFilenames: tmpHashedFilenames,
|
|
199
|
+
CachePaths:
|
|
200
|
+
{
|
|
201
|
+
Root: tmpCacheRoot,
|
|
202
|
+
Thumbnails: tmpCacheThumbnails,
|
|
203
|
+
Archives: tmpCacheArchives,
|
|
204
|
+
VideoFrames: tmpCacheVideoFrames,
|
|
205
|
+
AudioWaveforms: tmpCacheAudioWaveforms
|
|
206
|
+
},
|
|
207
|
+
ArchiveSupport:
|
|
208
|
+
{
|
|
209
|
+
Enabled: true,
|
|
210
|
+
Has7z: tmpArchiveService.has7z,
|
|
211
|
+
NativeZipOnly: !tmpArchiveService.has7z,
|
|
212
|
+
SupportedExtensions: tmpArchiveService.getSupportedExtensions()
|
|
213
|
+
}
|
|
137
214
|
});
|
|
138
215
|
return fNext();
|
|
139
216
|
});
|
|
@@ -178,6 +255,49 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
178
255
|
});
|
|
179
256
|
}
|
|
180
257
|
|
|
258
|
+
// Archive annotation middleware: wrap /api/filebrowser/list responses to
|
|
259
|
+
// change Type: 'file' to Type: 'archive' for entries with archive extensions.
|
|
260
|
+
// This makes archive files appear as navigable containers to the client.
|
|
261
|
+
tmpServiceServer.server.use(
|
|
262
|
+
(pRequest, pResponse, fNext) =>
|
|
263
|
+
{
|
|
264
|
+
if (!pRequest.url.startsWith('/api/filebrowser/list'))
|
|
265
|
+
{
|
|
266
|
+
return fNext();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let tmpOriginalSendArchive = pResponse.send.bind(pResponse);
|
|
270
|
+
let tmpSendWrapped = pResponse.send;
|
|
271
|
+
|
|
272
|
+
// Wrap send — there may already be a wrapper from hashed filenames.
|
|
273
|
+
// We chain on top of whatever send is currently set.
|
|
274
|
+
let tmpPreviousSend = pResponse.send;
|
|
275
|
+
pResponse.send = function (pStatusOrData, pData)
|
|
276
|
+
{
|
|
277
|
+
let tmpFileList = (typeof (pStatusOrData) === 'number') ? pData : pStatusOrData;
|
|
278
|
+
|
|
279
|
+
if (Array.isArray(tmpFileList))
|
|
280
|
+
{
|
|
281
|
+
for (let i = 0; i < tmpFileList.length; i++)
|
|
282
|
+
{
|
|
283
|
+
let tmpEntry = tmpFileList[i];
|
|
284
|
+
if (tmpEntry.Type === 'file' && tmpEntry.Extension)
|
|
285
|
+
{
|
|
286
|
+
if (tmpArchiveService.isArchiveFile(tmpEntry.Extension)
|
|
287
|
+
&& tmpArchiveService.canHandle(tmpEntry.Extension))
|
|
288
|
+
{
|
|
289
|
+
tmpEntry.Type = 'archive';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return tmpPreviousSend.call(pResponse, pStatusOrData, pData);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return fNext();
|
|
299
|
+
});
|
|
300
|
+
|
|
181
301
|
// Connect file browser API routes
|
|
182
302
|
tmpFileBrowser.connectRoutes();
|
|
183
303
|
|
|
@@ -203,6 +323,473 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
203
323
|
// Connect media service API routes
|
|
204
324
|
tmpMediaService.connectRoutes(tmpServiceServer);
|
|
205
325
|
|
|
326
|
+
// --- GET /api/media/video-frames ---
|
|
327
|
+
// Extract evenly-spaced frames from a video for the Video Explorer.
|
|
328
|
+
tmpServiceServer.get('/api/media/video-frames',
|
|
329
|
+
(pRequest, pResponse, fNext) =>
|
|
330
|
+
{
|
|
331
|
+
try
|
|
332
|
+
{
|
|
333
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
334
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
335
|
+
let tmpRelPath = tmpQuery.path;
|
|
336
|
+
|
|
337
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
338
|
+
{
|
|
339
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
340
|
+
return fNext();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Sanitize
|
|
344
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
345
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
346
|
+
{
|
|
347
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
348
|
+
return fNext();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
352
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
353
|
+
{
|
|
354
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
355
|
+
return fNext();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
tmpVideoFrameService.extractFrames(tmpAbsPath, tmpRelPath,
|
|
359
|
+
{
|
|
360
|
+
count: tmpQuery.count,
|
|
361
|
+
width: tmpQuery.width,
|
|
362
|
+
height: tmpQuery.height,
|
|
363
|
+
format: tmpQuery.format
|
|
364
|
+
},
|
|
365
|
+
(pError, pResult) =>
|
|
366
|
+
{
|
|
367
|
+
if (pError)
|
|
368
|
+
{
|
|
369
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
370
|
+
return fNext();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
pResponse.send(pResult);
|
|
374
|
+
return fNext();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
catch (pError)
|
|
378
|
+
{
|
|
379
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
380
|
+
return fNext();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// --- GET /api/media/video-frame/:cacheKey/:filename ---
|
|
385
|
+
// Serve a single cached video frame image.
|
|
386
|
+
tmpServiceServer.get('/api/media/video-frame/:cacheKey/:filename',
|
|
387
|
+
(pRequest, pResponse, fNext) =>
|
|
388
|
+
{
|
|
389
|
+
try
|
|
390
|
+
{
|
|
391
|
+
let tmpCacheKey = pRequest.params.cacheKey;
|
|
392
|
+
let tmpFilename = pRequest.params.filename;
|
|
393
|
+
|
|
394
|
+
let tmpFramePath = tmpVideoFrameService.getFramePath(tmpCacheKey, tmpFilename);
|
|
395
|
+
|
|
396
|
+
if (!tmpFramePath)
|
|
397
|
+
{
|
|
398
|
+
pResponse.send(404, { Success: false, Error: 'Frame not found.' });
|
|
399
|
+
return fNext();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let tmpStat = libFs.statSync(tmpFramePath);
|
|
403
|
+
let tmpExt = libPath.extname(tmpFilename).toLowerCase();
|
|
404
|
+
let tmpMimeTypes = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp' };
|
|
405
|
+
let tmpMime = tmpMimeTypes[tmpExt] || 'image/jpeg';
|
|
406
|
+
|
|
407
|
+
pResponse.writeHead(200,
|
|
408
|
+
{
|
|
409
|
+
'Content-Type': tmpMime,
|
|
410
|
+
'Content-Length': tmpStat.size,
|
|
411
|
+
'Cache-Control': 'public, max-age=86400'
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
let tmpStream = libFs.createReadStream(tmpFramePath);
|
|
415
|
+
tmpStream.pipe(pResponse);
|
|
416
|
+
tmpStream.on('end', () => { return fNext(false); });
|
|
417
|
+
tmpStream.on('error', () =>
|
|
418
|
+
{
|
|
419
|
+
pResponse.send(500, { Error: 'Failed to serve frame.' });
|
|
420
|
+
return fNext(false);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (pError)
|
|
424
|
+
{
|
|
425
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
426
|
+
return fNext();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// --- GET /api/media/video-frame-at ---
|
|
431
|
+
// Extract a single frame at an arbitrary timestamp (for timeline click).
|
|
432
|
+
tmpServiceServer.get('/api/media/video-frame-at',
|
|
433
|
+
(pRequest, pResponse, fNext) =>
|
|
434
|
+
{
|
|
435
|
+
try
|
|
436
|
+
{
|
|
437
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
438
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
439
|
+
let tmpRelPath = tmpQuery.path;
|
|
440
|
+
let tmpCacheKey = tmpQuery.cacheKey;
|
|
441
|
+
let tmpTimestamp = parseFloat(tmpQuery.timestamp);
|
|
442
|
+
|
|
443
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
444
|
+
{
|
|
445
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
446
|
+
return fNext();
|
|
447
|
+
}
|
|
448
|
+
if (!tmpCacheKey || typeof (tmpCacheKey) !== 'string')
|
|
449
|
+
{
|
|
450
|
+
pResponse.send(400, { Success: false, Error: 'Missing cacheKey parameter.' });
|
|
451
|
+
return fNext();
|
|
452
|
+
}
|
|
453
|
+
if (isNaN(tmpTimestamp) || tmpTimestamp < 0)
|
|
454
|
+
{
|
|
455
|
+
pResponse.send(400, { Success: false, Error: 'Invalid timestamp.' });
|
|
456
|
+
return fNext();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Sanitize
|
|
460
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
461
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
462
|
+
{
|
|
463
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
464
|
+
return fNext();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
468
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
469
|
+
{
|
|
470
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
471
|
+
return fNext();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
tmpVideoFrameService.extractSingleFrame(tmpAbsPath, tmpCacheKey, tmpTimestamp,
|
|
475
|
+
{
|
|
476
|
+
width: tmpQuery.width,
|
|
477
|
+
height: tmpQuery.height,
|
|
478
|
+
format: tmpQuery.format
|
|
479
|
+
},
|
|
480
|
+
(pError, pResult) =>
|
|
481
|
+
{
|
|
482
|
+
if (pError)
|
|
483
|
+
{
|
|
484
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
485
|
+
return fNext();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
pResponse.send(pResult);
|
|
489
|
+
return fNext();
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (pError)
|
|
493
|
+
{
|
|
494
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
495
|
+
return fNext();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// --- GET /api/media/audio-waveform ---
|
|
500
|
+
// Extract waveform peak data from an audio file for the Audio Explorer.
|
|
501
|
+
tmpServiceServer.get('/api/media/audio-waveform',
|
|
502
|
+
(pRequest, pResponse, fNext) =>
|
|
503
|
+
{
|
|
504
|
+
try
|
|
505
|
+
{
|
|
506
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
507
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
508
|
+
let tmpRelPath = tmpQuery.path;
|
|
509
|
+
|
|
510
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
511
|
+
{
|
|
512
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
513
|
+
return fNext();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Sanitize
|
|
517
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
518
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
519
|
+
{
|
|
520
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
521
|
+
return fNext();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
525
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
526
|
+
{
|
|
527
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
528
|
+
return fNext();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
tmpAudioWaveformService.extractWaveform(tmpAbsPath, tmpRelPath,
|
|
532
|
+
{
|
|
533
|
+
peaks: tmpQuery.peaks
|
|
534
|
+
},
|
|
535
|
+
(pError, pResult) =>
|
|
536
|
+
{
|
|
537
|
+
if (pError)
|
|
538
|
+
{
|
|
539
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
540
|
+
return fNext();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
pResponse.send(pResult);
|
|
544
|
+
return fNext();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch (pError)
|
|
548
|
+
{
|
|
549
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
550
|
+
return fNext();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// --- GET /api/media/audio-segment ---
|
|
555
|
+
// Extract an audio sub-clip for remote playback.
|
|
556
|
+
tmpServiceServer.get('/api/media/audio-segment',
|
|
557
|
+
(pRequest, pResponse, fNext) =>
|
|
558
|
+
{
|
|
559
|
+
try
|
|
560
|
+
{
|
|
561
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
562
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
563
|
+
let tmpRelPath = tmpQuery.path;
|
|
564
|
+
|
|
565
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
566
|
+
{
|
|
567
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
568
|
+
return fNext();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Sanitize
|
|
572
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
573
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
574
|
+
{
|
|
575
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
576
|
+
return fNext();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
580
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
581
|
+
{
|
|
582
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
583
|
+
return fNext();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
tmpAudioWaveformService.extractSegment(tmpAbsPath, tmpRelPath,
|
|
587
|
+
{
|
|
588
|
+
start: tmpQuery.start,
|
|
589
|
+
end: tmpQuery.end,
|
|
590
|
+
format: tmpQuery.format
|
|
591
|
+
},
|
|
592
|
+
(pError, pResult) =>
|
|
593
|
+
{
|
|
594
|
+
if (pError)
|
|
595
|
+
{
|
|
596
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
597
|
+
return fNext();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Stream the segment file back
|
|
601
|
+
try
|
|
602
|
+
{
|
|
603
|
+
let tmpSegPath = pResult.SegmentPath;
|
|
604
|
+
let tmpSegStat = libFs.statSync(tmpSegPath);
|
|
605
|
+
let tmpMimeMap = { 'mp3': 'audio/mpeg', 'aac': 'audio/aac', 'ogg': 'audio/ogg', 'wav': 'audio/wav', 'flac': 'audio/flac' };
|
|
606
|
+
let tmpMime = tmpMimeMap[pResult.Format] || 'audio/mpeg';
|
|
607
|
+
|
|
608
|
+
pResponse.writeHead(200,
|
|
609
|
+
{
|
|
610
|
+
'Content-Type': tmpMime,
|
|
611
|
+
'Content-Length': tmpSegStat.size,
|
|
612
|
+
'Cache-Control': 'public, max-age=86400',
|
|
613
|
+
'Content-Disposition': `inline; filename="segment_${pResult.Start.toFixed(0)}-${pResult.End.toFixed(0)}.${pResult.Format}"`
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
let tmpStream = libFs.createReadStream(tmpSegPath);
|
|
617
|
+
tmpStream.pipe(pResponse);
|
|
618
|
+
tmpStream.on('end', () => { return fNext(false); });
|
|
619
|
+
tmpStream.on('error', () =>
|
|
620
|
+
{
|
|
621
|
+
pResponse.send(500, { Error: 'Failed to serve segment.' });
|
|
622
|
+
return fNext(false);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
catch (pStreamError)
|
|
626
|
+
{
|
|
627
|
+
pResponse.send(500, { Success: false, Error: pStreamError.message });
|
|
628
|
+
return fNext();
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch (pError)
|
|
633
|
+
{
|
|
634
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
635
|
+
return fNext();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// --- GET /api/media/ebook-convert ---
|
|
640
|
+
// Convert an ebook (MOBI, AZW, etc.) to EPUB for in-browser reading.
|
|
641
|
+
tmpServiceServer.get('/api/media/ebook-convert',
|
|
642
|
+
(pRequest, pResponse, fNext) =>
|
|
643
|
+
{
|
|
644
|
+
try
|
|
645
|
+
{
|
|
646
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
647
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
648
|
+
let tmpRelPath = tmpQuery.path;
|
|
649
|
+
|
|
650
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
651
|
+
{
|
|
652
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
653
|
+
return fNext();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Sanitize
|
|
657
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
658
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
659
|
+
{
|
|
660
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
661
|
+
return fNext();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
665
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
666
|
+
{
|
|
667
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
668
|
+
return fNext();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
tmpEbookService.convertToEpub(tmpAbsPath, tmpRelPath,
|
|
672
|
+
(pError, pResult) =>
|
|
673
|
+
{
|
|
674
|
+
if (pError)
|
|
675
|
+
{
|
|
676
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
677
|
+
return fNext();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
pResponse.send(pResult);
|
|
681
|
+
return fNext();
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch (pError)
|
|
685
|
+
{
|
|
686
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
687
|
+
return fNext();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// --- GET /api/media/ebook/:cacheKey/:filename ---
|
|
692
|
+
// Serve a cached converted ebook file.
|
|
693
|
+
tmpServiceServer.get('/api/media/ebook/:cacheKey/:filename',
|
|
694
|
+
(pRequest, pResponse, fNext) =>
|
|
695
|
+
{
|
|
696
|
+
try
|
|
697
|
+
{
|
|
698
|
+
let tmpCacheKey = pRequest.params.cacheKey;
|
|
699
|
+
let tmpFilename = pRequest.params.filename;
|
|
700
|
+
|
|
701
|
+
let tmpEbookPath = tmpEbookService.getConvertedPath(tmpCacheKey, tmpFilename);
|
|
702
|
+
|
|
703
|
+
if (!tmpEbookPath)
|
|
704
|
+
{
|
|
705
|
+
pResponse.send(404, { Success: false, Error: 'Ebook not found.' });
|
|
706
|
+
return fNext();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let tmpStat = libFs.statSync(tmpEbookPath);
|
|
710
|
+
|
|
711
|
+
pResponse.writeHead(200,
|
|
712
|
+
{
|
|
713
|
+
'Content-Type': 'application/epub+zip',
|
|
714
|
+
'Content-Length': tmpStat.size,
|
|
715
|
+
'Cache-Control': 'public, max-age=86400'
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
let tmpStream = libFs.createReadStream(tmpEbookPath);
|
|
719
|
+
tmpStream.pipe(pResponse);
|
|
720
|
+
tmpStream.on('end', () => { return fNext(false); });
|
|
721
|
+
tmpStream.on('error', () =>
|
|
722
|
+
{
|
|
723
|
+
pResponse.send(500, { Error: 'Failed to serve ebook.' });
|
|
724
|
+
return fNext(false);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
catch (pError)
|
|
728
|
+
{
|
|
729
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
730
|
+
return fNext();
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// --- POST /api/media/open ---
|
|
735
|
+
// Open a media file with an external application (e.g. VLC).
|
|
736
|
+
tmpServiceServer.post('/api/media/open',
|
|
737
|
+
(pRequest, pResponse, fNext) =>
|
|
738
|
+
{
|
|
739
|
+
try
|
|
740
|
+
{
|
|
741
|
+
let tmpBody = pRequest.body || {};
|
|
742
|
+
let tmpRelPath = tmpBody.path;
|
|
743
|
+
|
|
744
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
745
|
+
{
|
|
746
|
+
pResponse.send(400, { Success: false, Error: 'Missing or invalid path.' });
|
|
747
|
+
return fNext();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Sanitize: no absolute paths, no directory traversal
|
|
751
|
+
if (tmpRelPath.indexOf('..') !== -1 || libPath.isAbsolute(tmpRelPath))
|
|
752
|
+
{
|
|
753
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
754
|
+
return fNext();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
758
|
+
|
|
759
|
+
// Verify file exists
|
|
760
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
761
|
+
{
|
|
762
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
763
|
+
return fNext();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Determine the open command based on platform
|
|
767
|
+
let tmpSpawnArgs;
|
|
768
|
+
if (process.platform === 'darwin')
|
|
769
|
+
{
|
|
770
|
+
tmpSpawnArgs = ['open', ['-a', 'VLC', tmpAbsPath]];
|
|
771
|
+
}
|
|
772
|
+
else
|
|
773
|
+
{
|
|
774
|
+
tmpSpawnArgs = ['vlc', [tmpAbsPath]];
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let tmpChild = libChildProcess.spawn(tmpSpawnArgs[0], tmpSpawnArgs[1],
|
|
778
|
+
{
|
|
779
|
+
detached: true,
|
|
780
|
+
stdio: 'ignore'
|
|
781
|
+
});
|
|
782
|
+
tmpChild.unref();
|
|
783
|
+
|
|
784
|
+
pResponse.send({ Success: true });
|
|
785
|
+
}
|
|
786
|
+
catch (pError)
|
|
787
|
+
{
|
|
788
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
789
|
+
}
|
|
790
|
+
return fNext();
|
|
791
|
+
});
|
|
792
|
+
|
|
206
793
|
// Content-hashed URL rewrite: resolve /content-hashed/<hash>
|
|
207
794
|
// to /content/<resolved-path> so the static route serves the file.
|
|
208
795
|
// Uses server.pre() to rewrite BEFORE route matching.
|
|
@@ -232,6 +819,198 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
232
819
|
});
|
|
233
820
|
}
|
|
234
821
|
|
|
822
|
+
// Archive file listing: intercept /api/filebrowser/list requests
|
|
823
|
+
// where the path crosses into an archive. Uses server.pre() to
|
|
824
|
+
// respond before the normal file browser route handler.
|
|
825
|
+
tmpServiceServer.server.pre(
|
|
826
|
+
(pRequest, pResponse, fNext) =>
|
|
827
|
+
{
|
|
828
|
+
if (!pRequest.url.startsWith('/api/filebrowser/list'))
|
|
829
|
+
{
|
|
830
|
+
return fNext();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
834
|
+
let tmpPathParam = (tmpParsedUrl.query && tmpParsedUrl.query.path) || '';
|
|
835
|
+
|
|
836
|
+
// Resolve hash to path if hashed filenames is enabled
|
|
837
|
+
// (server.pre runs before server.use middleware, so hash resolution hasn't happened yet)
|
|
838
|
+
if (tmpHashedFilenames && /^[a-f0-9]{10}$/.test(tmpPathParam))
|
|
839
|
+
{
|
|
840
|
+
let tmpResolved = tmpPathRegistry.resolve(tmpPathParam);
|
|
841
|
+
if (tmpResolved !== null)
|
|
842
|
+
{
|
|
843
|
+
tmpPathParam = tmpResolved;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
let tmpArchiveInfo = tmpArchiveService.parseArchivePath(tmpPathParam);
|
|
848
|
+
|
|
849
|
+
if (!tmpArchiveInfo)
|
|
850
|
+
{
|
|
851
|
+
return fNext();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let tmpArchiveAbsPath = libPath.join(tmpContentPath, tmpArchiveInfo.archivePath);
|
|
855
|
+
|
|
856
|
+
if (!libFs.existsSync(tmpArchiveAbsPath))
|
|
857
|
+
{
|
|
858
|
+
pResponse.send(404, { Error: 'Archive not found.' });
|
|
859
|
+
return fNext(false);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
tmpArchiveService.listContents(
|
|
863
|
+
tmpArchiveAbsPath, tmpArchiveInfo.innerPath, tmpArchiveInfo.archivePath,
|
|
864
|
+
(pError, pFileList) =>
|
|
865
|
+
{
|
|
866
|
+
if (pError)
|
|
867
|
+
{
|
|
868
|
+
pResponse.send(400, { Error: pError.message });
|
|
869
|
+
return fNext(false);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Annotate with hashes if enabled
|
|
873
|
+
if (tmpHashedFilenames)
|
|
874
|
+
{
|
|
875
|
+
tmpPathRegistry.annotateFileList(pFileList);
|
|
876
|
+
|
|
877
|
+
// Register the archive inner path as a folder
|
|
878
|
+
let tmpFolderPath = tmpArchiveInfo.innerPath
|
|
879
|
+
? (tmpArchiveInfo.archivePath + '/' + tmpArchiveInfo.innerPath)
|
|
880
|
+
: tmpArchiveInfo.archivePath;
|
|
881
|
+
let tmpFolderHash = tmpPathRegistry.register(tmpFolderPath);
|
|
882
|
+
pResponse.header('X-Retold-Folder-Hash', tmpFolderHash);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
pResponse.send(pFileList);
|
|
886
|
+
return fNext(false);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Archive content serving: intercept /content/* requests that cross
|
|
891
|
+
// into an archive. Extracts the file to cache and streams it back.
|
|
892
|
+
tmpServiceServer.server.pre(
|
|
893
|
+
(pRequest, pResponse, fNext) =>
|
|
894
|
+
{
|
|
895
|
+
if (!pRequest.url.startsWith('/content/'))
|
|
896
|
+
{
|
|
897
|
+
return fNext();
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let tmpRelPath = decodeURIComponent(pRequest.url.replace(/^\/content\//, ''));
|
|
901
|
+
let tmpArchiveInfo = tmpArchiveService.parseArchivePath(tmpRelPath);
|
|
902
|
+
|
|
903
|
+
if (!tmpArchiveInfo || !tmpArchiveInfo.innerPath)
|
|
904
|
+
{
|
|
905
|
+
return fNext();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let tmpArchiveAbsPath = libPath.join(tmpContentPath, tmpArchiveInfo.archivePath);
|
|
909
|
+
|
|
910
|
+
tmpArchiveService.extractFile(
|
|
911
|
+
tmpArchiveAbsPath, tmpArchiveInfo.innerPath,
|
|
912
|
+
(pError, pCachedPath) =>
|
|
913
|
+
{
|
|
914
|
+
if (pError || !pCachedPath)
|
|
915
|
+
{
|
|
916
|
+
pResponse.send(404, { Error: 'Could not extract file from archive.' });
|
|
917
|
+
return fNext(false);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Stream the cached file
|
|
921
|
+
try
|
|
922
|
+
{
|
|
923
|
+
let tmpStat = libFs.statSync(pCachedPath);
|
|
924
|
+
let tmpExt = libPath.extname(pCachedPath).toLowerCase();
|
|
925
|
+
let tmpMime = tmpArchiveService.getMimeType(tmpExt);
|
|
926
|
+
|
|
927
|
+
pResponse.writeHead(200,
|
|
928
|
+
{
|
|
929
|
+
'Content-Type': tmpMime,
|
|
930
|
+
'Content-Length': tmpStat.size,
|
|
931
|
+
'Cache-Control': 'public, max-age=3600'
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
let tmpStream = libFs.createReadStream(pCachedPath);
|
|
935
|
+
tmpStream.pipe(pResponse);
|
|
936
|
+
tmpStream.on('end', () => { return fNext(false); });
|
|
937
|
+
tmpStream.on('error', () =>
|
|
938
|
+
{
|
|
939
|
+
pResponse.send(500, { Error: 'Failed to serve extracted file.' });
|
|
940
|
+
return fNext(false);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
catch (pStreamError)
|
|
944
|
+
{
|
|
945
|
+
pResponse.send(500, { Error: pStreamError.message });
|
|
946
|
+
return fNext(false);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// Archive-aware thumbnail/probe: intercept /api/media/thumbnail and
|
|
952
|
+
// /api/media/probe requests for files inside archives. Extracts the
|
|
953
|
+
// file to cache first, then rewrites the URL to point at the cached
|
|
954
|
+
// copy so the normal MediaService handler processes it.
|
|
955
|
+
tmpServiceServer.server.pre(
|
|
956
|
+
(pRequest, pResponse, fNext) =>
|
|
957
|
+
{
|
|
958
|
+
if (!pRequest.url.startsWith('/api/media/thumbnail')
|
|
959
|
+
&& !pRequest.url.startsWith('/api/media/probe'))
|
|
960
|
+
{
|
|
961
|
+
return fNext();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
965
|
+
let tmpPathParam = (tmpParsedUrl.query && tmpParsedUrl.query.path) || '';
|
|
966
|
+
|
|
967
|
+
// Resolve hash to path if hashed filenames is enabled
|
|
968
|
+
if (tmpHashedFilenames && /^[a-f0-9]{10}$/.test(tmpPathParam))
|
|
969
|
+
{
|
|
970
|
+
let tmpResolved = tmpPathRegistry.resolve(tmpPathParam);
|
|
971
|
+
if (tmpResolved !== null)
|
|
972
|
+
{
|
|
973
|
+
tmpPathParam = tmpResolved;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
let tmpArchiveInfo = tmpArchiveService.parseArchivePath(tmpPathParam);
|
|
978
|
+
|
|
979
|
+
if (!tmpArchiveInfo || !tmpArchiveInfo.innerPath)
|
|
980
|
+
{
|
|
981
|
+
return fNext();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let tmpArchiveAbsPath = libPath.join(tmpContentPath, tmpArchiveInfo.archivePath);
|
|
985
|
+
|
|
986
|
+
tmpArchiveService.extractFile(
|
|
987
|
+
tmpArchiveAbsPath, tmpArchiveInfo.innerPath,
|
|
988
|
+
(pError, pCachedPath) =>
|
|
989
|
+
{
|
|
990
|
+
if (pError || !pCachedPath)
|
|
991
|
+
{
|
|
992
|
+
pResponse.send(404, { Error: 'Could not extract file from archive.' });
|
|
993
|
+
return fNext(false);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Rewrite the path query param to point at the cached file
|
|
997
|
+
// relative to the content root. The MediaService resolves
|
|
998
|
+
// paths relative to ContentPath, so we need to compute the
|
|
999
|
+
// relative path from contentPath to the cached file.
|
|
1000
|
+
let tmpRelCached = libPath.relative(tmpContentPath, pCachedPath);
|
|
1001
|
+
|
|
1002
|
+
tmpParsedUrl.query.path = tmpRelCached;
|
|
1003
|
+
delete tmpParsedUrl.search;
|
|
1004
|
+
pRequest.url = libUrl.format(tmpParsedUrl);
|
|
1005
|
+
if (pRequest.query)
|
|
1006
|
+
{
|
|
1007
|
+
pRequest.query.path = tmpRelCached;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return fNext();
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
235
1014
|
// Serve content files at /content/ (for direct media access)
|
|
236
1015
|
tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
|
|
237
1016
|
|
|
@@ -247,6 +1026,9 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
247
1026
|
Fable: tmpFable,
|
|
248
1027
|
Orator: tmpOrator,
|
|
249
1028
|
MediaService: tmpMediaService,
|
|
1029
|
+
ArchiveService: tmpArchiveService,
|
|
1030
|
+
VideoFrameService: tmpVideoFrameService,
|
|
1031
|
+
AudioWaveformService: tmpAudioWaveformService,
|
|
250
1032
|
PathRegistry: tmpPathRegistry,
|
|
251
1033
|
Port: tmpPort
|
|
252
1034
|
});
|