retold-remote 0.0.13 → 0.0.15
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 +75 -0
- package/docs/_sidebar.md +2 -0
- package/docs/ultravisor-configuration.md +212 -0
- package/docs/ultravisor-integration.md +140 -0
- package/package.json +121 -96
- package/source/cli/RetoldRemote-Server-Setup.js +26 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +11 -3
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +5 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +16 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +101 -23
- package/source/server/RetoldRemote-EbookService.js +119 -6
- package/source/server/RetoldRemote-ImageService.js +144 -0
- package/source/server/RetoldRemote-MediaService.js +208 -34
- package/source/server/RetoldRemote-ToolDetector.js +27 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +288 -0
- package/source/server/RetoldRemote-VideoFrameService.js +309 -77
- package/source/views/PictView-Remote-AudioExplorer.js +28 -14
- package/source/views/PictView-Remote-ImageExplorer.js +31 -11
- package/source/views/PictView-Remote-VLCSetup.js +22 -12
- package/source/views/PictView-Remote-VideoExplorer.js +29 -14
- package/web-application/css/retold-remote.css +75 -0
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/ultravisor-configuration.md +212 -0
- package/web-application/docs/ultravisor-integration.md +140 -0
- package/web-application/retold-remote.js +58 -22
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +6 -6
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -48,6 +48,7 @@ const libRetoldRemoteMetadataCache = require('../server/RetoldRemote-MetadataCac
|
|
|
48
48
|
const libRetoldRemoteFileOperationService = require('../server/RetoldRemote-FileOperationService.js');
|
|
49
49
|
const libRetoldRemoteAISortService = require('../server/RetoldRemote-AISortService.js');
|
|
50
50
|
const libRetoldRemoteImageService = require('../server/RetoldRemote-ImageService.js');
|
|
51
|
+
const libRetoldRemoteUltravisorDispatcher = require('../server/RetoldRemote-UltravisorDispatcher.js');
|
|
51
52
|
const libUrl = require('url');
|
|
52
53
|
|
|
53
54
|
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
@@ -83,6 +84,16 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
83
84
|
tmpSettings.ParimeCacheServer = pOptions.CacheServer;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// If an Ultravisor URL is specified, offload heavy processing to beacons
|
|
88
|
+
if (pOptions.UltravisorURL)
|
|
89
|
+
{
|
|
90
|
+
tmpSettings.UltravisorURL = pOptions.UltravisorURL;
|
|
91
|
+
}
|
|
92
|
+
if (pOptions.ContentAPIURL)
|
|
93
|
+
{
|
|
94
|
+
tmpSettings.ContentAPIURL = pOptions.ContentAPIURL;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
let tmpFable = new libFable(tmpSettings);
|
|
87
98
|
|
|
88
99
|
// Ensure the content directory exists
|
|
@@ -201,6 +212,16 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
201
212
|
PathRegistry: tmpPathRegistry
|
|
202
213
|
});
|
|
203
214
|
|
|
215
|
+
// Set up the Ultravisor dispatcher for offloading heavy processing
|
|
216
|
+
let tmpDispatcher = new libRetoldRemoteUltravisorDispatcher(tmpFable, {});
|
|
217
|
+
|
|
218
|
+
// Wire the dispatcher to services that can offload processing
|
|
219
|
+
tmpMediaService.setDispatcher(tmpDispatcher);
|
|
220
|
+
tmpVideoFrameService.setDispatcher(tmpDispatcher);
|
|
221
|
+
tmpAudioWaveformService.setDispatcher(tmpDispatcher);
|
|
222
|
+
tmpEbookService.setDispatcher(tmpDispatcher);
|
|
223
|
+
tmpImageService.setDispatcher(tmpDispatcher);
|
|
224
|
+
|
|
204
225
|
// Share tool capabilities with the image service so it can
|
|
205
226
|
// use dcraw and ImageMagick for raw camera format conversion.
|
|
206
227
|
// Also provides the centrally-verified sharp module reference.
|
|
@@ -458,6 +479,10 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
458
479
|
{
|
|
459
480
|
tmpFable.log.info('EPUB metadata service initialized.');
|
|
460
481
|
});
|
|
482
|
+
tmpDispatcher.checkConnection(() =>
|
|
483
|
+
{
|
|
484
|
+
// Non-fatal — if Ultravisor is down, processing stays local
|
|
485
|
+
});
|
|
461
486
|
|
|
462
487
|
// --- GET /api/media/metadata ---
|
|
463
488
|
// Get cached metadata (with ID3/format tags) for a single file.
|
|
@@ -2114,6 +2139,7 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2114
2139
|
ParimeCache: tmpParimeCache,
|
|
2115
2140
|
MetadataCache: tmpMetadataCache,
|
|
2116
2141
|
FileOperationService: tmpFileOperationService,
|
|
2142
|
+
UltravisorDispatcher: tmpDispatcher,
|
|
2117
2143
|
Port: tmpPort
|
|
2118
2144
|
});
|
|
2119
2145
|
});
|
|
@@ -563,6 +563,12 @@ class GalleryNavigationProvider extends libPictProvider
|
|
|
563
563
|
let tmpGalleryContainer = document.getElementById('RetoldRemote-Gallery-Container');
|
|
564
564
|
let tmpViewerContainer = document.getElementById('RetoldRemote-Viewer-Container');
|
|
565
565
|
|
|
566
|
+
// Stop any playing video/audio and release resources before hiding the viewer
|
|
567
|
+
let tmpVideo = document.querySelector('#RetoldRemote-Viewer-Container video');
|
|
568
|
+
let tmpAudio = document.querySelector('#RetoldRemote-Viewer-Container audio');
|
|
569
|
+
if (tmpVideo) { tmpVideo.pause(); tmpVideo.removeAttribute('src'); tmpVideo.load(); }
|
|
570
|
+
if (tmpAudio) { tmpAudio.pause(); tmpAudio.removeAttribute('src'); tmpAudio.load(); }
|
|
571
|
+
|
|
566
572
|
if (tmpGalleryContainer) tmpGalleryContainer.style.display = '';
|
|
567
573
|
if (tmpViewerContainer) tmpViewerContainer.style.display = 'none';
|
|
568
574
|
|
|
@@ -1380,14 +1386,16 @@ class GalleryNavigationProvider extends libPictProvider
|
|
|
1380
1386
|
// iOS/iPadOS: use vlc-x-callback for reliable app launching
|
|
1381
1387
|
tmpVLCURL = 'vlc-x-callback://x-callback-url/stream?url=' + encodeURIComponent(tmpStreamURL);
|
|
1382
1388
|
}
|
|
1383
|
-
else if (
|
|
1389
|
+
else if (tmpIsAndroid)
|
|
1384
1390
|
{
|
|
1385
|
-
//
|
|
1391
|
+
// Android: VLC app handles the raw URL natively via intent
|
|
1386
1392
|
tmpVLCURL = 'vlc://' + tmpStreamURL;
|
|
1387
1393
|
}
|
|
1388
1394
|
else
|
|
1389
1395
|
{
|
|
1390
|
-
// macOS
|
|
1396
|
+
// Windows, macOS, Linux: encode the URL so the custom protocol
|
|
1397
|
+
// handler can decode it. On Windows this is required because the
|
|
1398
|
+
// shell strips the colon from nested http:// URLs.
|
|
1391
1399
|
tmpVLCURL = 'vlc://' + encodeURIComponent(tmpStreamURL);
|
|
1392
1400
|
}
|
|
1393
1401
|
|
|
@@ -133,6 +133,22 @@ function handleVideoExplorerKey(pGalleryNav, pEvent)
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
break;
|
|
136
|
+
|
|
137
|
+
case 'v':
|
|
138
|
+
pEvent.preventDefault();
|
|
139
|
+
pGalleryNav._streamWithVLC();
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case ' ':
|
|
143
|
+
pEvent.preventDefault();
|
|
144
|
+
{
|
|
145
|
+
let tmpPlayVEX = pGalleryNav.pict.views['RetoldRemote-VideoExplorer'];
|
|
146
|
+
if (tmpPlayVEX)
|
|
147
|
+
{
|
|
148
|
+
tmpPlayVEX.playInBrowser();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
|
|
@@ -51,6 +51,9 @@ class RetoldRemoteAudioWaveformService extends libFableServiceProviderBase
|
|
|
51
51
|
|
|
52
52
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
53
53
|
|
|
54
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
55
|
+
this._dispatcher = null;
|
|
56
|
+
|
|
54
57
|
// Detect audiowaveform availability
|
|
55
58
|
this.hasAudiowaveform = this._detectCommand('audiowaveform --version');
|
|
56
59
|
|
|
@@ -61,6 +64,16 @@ class RetoldRemoteAudioWaveformService extends libFableServiceProviderBase
|
|
|
61
64
|
this.fable.log.info(` audiowaveform tool: ${this.hasAudiowaveform ? 'available' : 'not found (using ffprobe fallback)'}`);
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
71
|
+
*/
|
|
72
|
+
setDispatcher(pDispatcher)
|
|
73
|
+
{
|
|
74
|
+
this._dispatcher = pDispatcher;
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/**
|
|
65
78
|
* Check if a command-line tool is available.
|
|
66
79
|
*
|
|
@@ -114,45 +127,76 @@ class RetoldRemoteAudioWaveformService extends libFableServiceProviderBase
|
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
129
|
* Probe an audio file with ffprobe to get metadata.
|
|
130
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
117
131
|
*
|
|
118
132
|
* @param {string} pAbsPath - Absolute path to the audio file
|
|
119
133
|
* @param {Function} fCallback - Callback(pError, { duration, sampleRate, channels, codec, bitrate, size })
|
|
120
134
|
*/
|
|
121
135
|
_probeAudio(pAbsPath, fCallback)
|
|
122
136
|
{
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
|
|
126
|
-
let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
|
|
127
|
-
let tmpData = JSON.parse(tmpOutput.toString());
|
|
128
|
-
|
|
129
|
-
let tmpResult = {};
|
|
137
|
+
let tmpSelf = this;
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
// Try Ultravisor dispatch first
|
|
140
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
141
|
+
{
|
|
142
|
+
let tmpRelPath;
|
|
143
|
+
try
|
|
132
144
|
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
146
|
+
}
|
|
147
|
+
catch (pErr)
|
|
148
|
+
{
|
|
149
|
+
tmpRelPath = null;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
if (
|
|
152
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
140
153
|
{
|
|
141
|
-
|
|
154
|
+
let tmpCommand = `ffprobe -v quiet -print_format json -show_format -show_streams "{SourcePath}"`;
|
|
155
|
+
|
|
156
|
+
this._dispatcher.dispatchMediaCommand(
|
|
157
|
+
{
|
|
158
|
+
Command: tmpCommand,
|
|
159
|
+
InputPath: tmpRelPath,
|
|
160
|
+
AffinityKey: tmpRelPath,
|
|
161
|
+
TimeoutMs: 30000
|
|
162
|
+
},
|
|
163
|
+
(pDispatchError, pResult) =>
|
|
142
164
|
{
|
|
143
|
-
|
|
144
|
-
if (tmpStream.codec_type === 'audio')
|
|
165
|
+
if (!pDispatchError && pResult && pResult.Outputs && pResult.Outputs.StdOut)
|
|
145
166
|
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
try
|
|
168
|
+
{
|
|
169
|
+
let tmpData = JSON.parse(pResult.Outputs.StdOut);
|
|
170
|
+
let tmpParsed = tmpSelf._parseAudioProbeData(tmpData);
|
|
171
|
+
tmpSelf.fable.log.info(`ffprobe (audio) via Ultravisor for ${tmpRelPath}`);
|
|
172
|
+
return fCallback(null, tmpParsed);
|
|
173
|
+
}
|
|
174
|
+
catch (pParseError)
|
|
175
|
+
{
|
|
176
|
+
// Fall through to local
|
|
177
|
+
}
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
|
|
180
|
+
tmpSelf._probeAudioLocal(pAbsPath, fCallback);
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
153
183
|
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this._probeAudioLocal(pAbsPath, fCallback);
|
|
187
|
+
}
|
|
154
188
|
|
|
155
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Probe an audio file locally with ffprobe.
|
|
191
|
+
*/
|
|
192
|
+
_probeAudioLocal(pAbsPath, fCallback)
|
|
193
|
+
{
|
|
194
|
+
try
|
|
195
|
+
{
|
|
196
|
+
let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
|
|
197
|
+
let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
|
|
198
|
+
let tmpData = JSON.parse(tmpOutput.toString());
|
|
199
|
+
return fCallback(null, this._parseAudioProbeData(tmpData));
|
|
156
200
|
}
|
|
157
201
|
catch (pError)
|
|
158
202
|
{
|
|
@@ -160,6 +204,40 @@ class RetoldRemoteAudioWaveformService extends libFableServiceProviderBase
|
|
|
160
204
|
}
|
|
161
205
|
}
|
|
162
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Parse ffprobe JSON output for audio metadata.
|
|
209
|
+
*/
|
|
210
|
+
_parseAudioProbeData(pData)
|
|
211
|
+
{
|
|
212
|
+
let tmpResult = {};
|
|
213
|
+
|
|
214
|
+
if (pData.format)
|
|
215
|
+
{
|
|
216
|
+
tmpResult.duration = parseFloat(pData.format.duration) || null;
|
|
217
|
+
tmpResult.bitrate = parseInt(pData.format.bit_rate, 10) || null;
|
|
218
|
+
tmpResult.size = parseInt(pData.format.size, 10) || null;
|
|
219
|
+
tmpResult.formatName = pData.format.format_name || null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (pData.streams)
|
|
223
|
+
{
|
|
224
|
+
for (let i = 0; i < pData.streams.length; i++)
|
|
225
|
+
{
|
|
226
|
+
let tmpStream = pData.streams[i];
|
|
227
|
+
if (tmpStream.codec_type === 'audio')
|
|
228
|
+
{
|
|
229
|
+
tmpResult.sampleRate = parseInt(tmpStream.sample_rate, 10) || null;
|
|
230
|
+
tmpResult.channels = tmpStream.channels || null;
|
|
231
|
+
tmpResult.codec = tmpStream.codec_name || null;
|
|
232
|
+
tmpResult.channelLayout = tmpStream.channel_layout || null;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return tmpResult;
|
|
239
|
+
}
|
|
240
|
+
|
|
163
241
|
/**
|
|
164
242
|
* Extract waveform peaks using BBC audiowaveform tool.
|
|
165
243
|
* Generates a JSON file with min/max peak pairs.
|
|
@@ -59,9 +59,22 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
59
59
|
|
|
60
60
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
61
61
|
|
|
62
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
63
|
+
this._dispatcher = null;
|
|
64
|
+
|
|
62
65
|
this.fable.log.info('Ebook Service: using ParimeBinaryStorage (category: ebook-cache)');
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
72
|
+
*/
|
|
73
|
+
setDispatcher(pDispatcher)
|
|
74
|
+
{
|
|
75
|
+
this._dispatcher = pDispatcher;
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Check if a file extension is convertible to EPUB.
|
|
67
80
|
*
|
|
@@ -146,25 +159,125 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
146
159
|
|
|
147
160
|
this.fable.log.info(`Converting ebook: ${pRelPath} -> EPUB`);
|
|
148
161
|
|
|
162
|
+
let _finishConversion = (pOutputPath, pOutputFilename, pCacheDir, pManifestPath) =>
|
|
163
|
+
{
|
|
164
|
+
if (!libFs.existsSync(pOutputPath))
|
|
165
|
+
{
|
|
166
|
+
return fCallback(new Error('Conversion completed but output file not found.'));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let tmpOutputStat = libFs.statSync(pOutputPath);
|
|
170
|
+
|
|
171
|
+
let tmpResult =
|
|
172
|
+
{
|
|
173
|
+
Success: true,
|
|
174
|
+
SourcePath: pRelPath,
|
|
175
|
+
CacheKey: libPath.basename(pCacheDir),
|
|
176
|
+
OutputFilename: pOutputFilename,
|
|
177
|
+
FileSize: tmpOutputStat.size,
|
|
178
|
+
ConvertedAt: new Date().toISOString()
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Write manifest to cache
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
185
|
+
}
|
|
186
|
+
catch (pWriteError)
|
|
187
|
+
{
|
|
188
|
+
tmpSelf.fable.log.warn(`Could not write ebook manifest: ${pWriteError.message}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
tmpSelf.fable.log.info(`Converted ebook: ${pRelPath} (${tmpOutputStat.size} bytes)`);
|
|
192
|
+
return fCallback(null, tmpResult);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Try Ultravisor dispatch first
|
|
196
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
197
|
+
{
|
|
198
|
+
let tmpRelPath;
|
|
199
|
+
try
|
|
200
|
+
{
|
|
201
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
202
|
+
}
|
|
203
|
+
catch (pErr)
|
|
204
|
+
{
|
|
205
|
+
tmpRelPath = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
209
|
+
{
|
|
210
|
+
let tmpCommand = `ebook-convert "{SourcePath}" "{OutputPath}"`;
|
|
211
|
+
|
|
212
|
+
this._dispatcher.dispatchMediaCommand(
|
|
213
|
+
{
|
|
214
|
+
Command: tmpCommand,
|
|
215
|
+
InputPath: tmpRelPath,
|
|
216
|
+
OutputFilename: tmpOutputFilename,
|
|
217
|
+
AffinityKey: tmpRelPath,
|
|
218
|
+
TimeoutMs: 180000
|
|
219
|
+
},
|
|
220
|
+
(pDispatchError, pResult) =>
|
|
221
|
+
{
|
|
222
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
223
|
+
{
|
|
224
|
+
try
|
|
225
|
+
{
|
|
226
|
+
libFs.writeFileSync(tmpOutputPath, pResult.OutputBuffer);
|
|
227
|
+
tmpSelf.fable.log.info(`Ebook converted via Ultravisor for ${tmpRelPath}`);
|
|
228
|
+
return _finishConversion(tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath);
|
|
229
|
+
}
|
|
230
|
+
catch (pWriteError)
|
|
231
|
+
{
|
|
232
|
+
// Fall through to local
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fall through to local processing
|
|
237
|
+
tmpSelf.fable.log.info(`Ultravisor dispatch failed for ebook conversion, falling back to local: ${pDispatchError ? pDispatchError.message : 'no output'}`);
|
|
238
|
+
tmpSelf._convertToEpubLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this._convertToEpubLocal(pAbsPath, tmpOutputPath, tmpOutputFilename, tmpCacheDir, tmpManifestPath, pRelPath, fCallback);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Convert an ebook to EPUB locally using ebook-convert.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} pAbsPath - Absolute path to the source ebook
|
|
251
|
+
* @param {string} pOutputPath - Absolute path for the output EPUB
|
|
252
|
+
* @param {string} pOutputFilename - Output filename
|
|
253
|
+
* @param {string} pCacheDir - Cache directory path
|
|
254
|
+
* @param {string} pManifestPath - Manifest file path
|
|
255
|
+
* @param {string} pRelPath - Relative path (for logging)
|
|
256
|
+
* @param {Function} fCallback - Callback(pError, pResult)
|
|
257
|
+
*/
|
|
258
|
+
_convertToEpubLocal(pAbsPath, pOutputPath, pOutputFilename, pCacheDir, pManifestPath, pRelPath, fCallback)
|
|
259
|
+
{
|
|
260
|
+
let tmpSelf = this;
|
|
261
|
+
|
|
149
262
|
try
|
|
150
263
|
{
|
|
151
264
|
// ebook-convert input.mobi output.epub
|
|
152
|
-
let tmpCmd = `ebook-convert "${pAbsPath}" "${
|
|
265
|
+
let tmpCmd = `ebook-convert "${pAbsPath}" "${pOutputPath}"`;
|
|
153
266
|
libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 120000 });
|
|
154
267
|
|
|
155
|
-
if (!libFs.existsSync(
|
|
268
|
+
if (!libFs.existsSync(pOutputPath))
|
|
156
269
|
{
|
|
157
270
|
return fCallback(new Error('Conversion completed but output file not found.'));
|
|
158
271
|
}
|
|
159
272
|
|
|
160
|
-
let tmpOutputStat = libFs.statSync(
|
|
273
|
+
let tmpOutputStat = libFs.statSync(pOutputPath);
|
|
161
274
|
|
|
162
275
|
let tmpResult =
|
|
163
276
|
{
|
|
164
277
|
Success: true,
|
|
165
278
|
SourcePath: pRelPath,
|
|
166
|
-
CacheKey: libPath.basename(
|
|
167
|
-
OutputFilename:
|
|
279
|
+
CacheKey: libPath.basename(pCacheDir),
|
|
280
|
+
OutputFilename: pOutputFilename,
|
|
168
281
|
FileSize: tmpOutputStat.size,
|
|
169
282
|
ConvertedAt: new Date().toISOString()
|
|
170
283
|
};
|
|
@@ -172,7 +285,7 @@ class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
|
172
285
|
// Write manifest to cache
|
|
173
286
|
try
|
|
174
287
|
{
|
|
175
|
-
libFs.writeFileSync(
|
|
288
|
+
libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
176
289
|
}
|
|
177
290
|
catch (pWriteError)
|
|
178
291
|
{
|
|
@@ -78,6 +78,19 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
78
78
|
|
|
79
79
|
// Tool capabilities — set by MediaService after ToolDetector runs
|
|
80
80
|
this._capabilities = {};
|
|
81
|
+
|
|
82
|
+
// Ultravisor dispatcher — set via setDispatcher()
|
|
83
|
+
this._dispatcher = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set the Ultravisor dispatcher for offloading heavy processing.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
|
|
90
|
+
*/
|
|
91
|
+
setDispatcher(pDispatcher)
|
|
92
|
+
{
|
|
93
|
+
this._dispatcher = pDispatcher;
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
/**
|
|
@@ -336,6 +349,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
336
349
|
|
|
337
350
|
/**
|
|
338
351
|
* Convert a raw image to JPEG using dcraw piped through Sharp.
|
|
352
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
339
353
|
* dcraw outputs PPM to stdout; Sharp converts to JPEG.
|
|
340
354
|
*
|
|
341
355
|
* @param {string} pAbsPath - Raw file path
|
|
@@ -344,6 +358,71 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
344
358
|
* @param {Function} fCallback - Callback(pError)
|
|
345
359
|
*/
|
|
346
360
|
_convertWithDcraw(pAbsPath, pOutputPath, pFullResolution, fCallback)
|
|
361
|
+
{
|
|
362
|
+
let tmpSelf = this;
|
|
363
|
+
|
|
364
|
+
// Try Ultravisor dispatch first
|
|
365
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
366
|
+
{
|
|
367
|
+
let tmpRelPath;
|
|
368
|
+
try
|
|
369
|
+
{
|
|
370
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
371
|
+
}
|
|
372
|
+
catch (pErr)
|
|
373
|
+
{
|
|
374
|
+
tmpRelPath = null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
378
|
+
{
|
|
379
|
+
let tmpHalfFlag = pFullResolution ? '' : ' -h';
|
|
380
|
+
let tmpOutputFilename = libPath.basename(pOutputPath);
|
|
381
|
+
let tmpCommand = `dcraw -c -w${tmpHalfFlag} "{SourcePath}" | convert ppm:- jpeg:"{OutputPath}"`;
|
|
382
|
+
|
|
383
|
+
this._dispatcher.dispatchMediaCommand(
|
|
384
|
+
{
|
|
385
|
+
Command: tmpCommand,
|
|
386
|
+
InputPath: tmpRelPath,
|
|
387
|
+
OutputFilename: tmpOutputFilename,
|
|
388
|
+
AffinityKey: tmpRelPath,
|
|
389
|
+
TimeoutMs: 180000
|
|
390
|
+
},
|
|
391
|
+
(pDispatchError, pResult) =>
|
|
392
|
+
{
|
|
393
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
394
|
+
{
|
|
395
|
+
try
|
|
396
|
+
{
|
|
397
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
398
|
+
if (!libFs.existsSync(tmpDir))
|
|
399
|
+
{
|
|
400
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
401
|
+
}
|
|
402
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
403
|
+
tmpSelf.fable.log.info(`Raw conversion via Ultravisor (dcraw) for ${tmpRelPath}`);
|
|
404
|
+
return fCallback(null);
|
|
405
|
+
}
|
|
406
|
+
catch (pWriteError)
|
|
407
|
+
{
|
|
408
|
+
// Fall through to local
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Fall through to local processing
|
|
413
|
+
tmpSelf._convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback);
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return this._convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Convert a raw image to JPEG locally using dcraw piped through Sharp.
|
|
424
|
+
*/
|
|
425
|
+
_convertWithDcrawLocal(pAbsPath, pOutputPath, pFullResolution, fCallback)
|
|
347
426
|
{
|
|
348
427
|
if (!this._capabilities.dcraw || !this._sharp)
|
|
349
428
|
{
|
|
@@ -413,6 +492,7 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
413
492
|
|
|
414
493
|
/**
|
|
415
494
|
* Convert a raw image to JPEG using ImageMagick's convert command.
|
|
495
|
+
* Tries Ultravisor dispatch first, falls back to local execution.
|
|
416
496
|
* Works if ImageMagick has the dcraw/ufraw delegate installed.
|
|
417
497
|
*
|
|
418
498
|
* @param {string} pAbsPath - Raw file path
|
|
@@ -420,6 +500,70 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
|
|
|
420
500
|
* @param {Function} fCallback - Callback(pError)
|
|
421
501
|
*/
|
|
422
502
|
_convertWithImageMagick(pAbsPath, pOutputPath, fCallback)
|
|
503
|
+
{
|
|
504
|
+
let tmpSelf = this;
|
|
505
|
+
|
|
506
|
+
// Try Ultravisor dispatch first
|
|
507
|
+
if (this._dispatcher && this._dispatcher.isAvailable())
|
|
508
|
+
{
|
|
509
|
+
let tmpRelPath;
|
|
510
|
+
try
|
|
511
|
+
{
|
|
512
|
+
tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
|
|
513
|
+
}
|
|
514
|
+
catch (pErr)
|
|
515
|
+
{
|
|
516
|
+
tmpRelPath = null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (tmpRelPath && !tmpRelPath.startsWith('..'))
|
|
520
|
+
{
|
|
521
|
+
let tmpOutputFilename = libPath.basename(pOutputPath);
|
|
522
|
+
let tmpCommand = `convert "{SourcePath}" -auto-orient -quality 92 "{OutputPath}"`;
|
|
523
|
+
|
|
524
|
+
this._dispatcher.dispatchMediaCommand(
|
|
525
|
+
{
|
|
526
|
+
Command: tmpCommand,
|
|
527
|
+
InputPath: tmpRelPath,
|
|
528
|
+
OutputFilename: tmpOutputFilename,
|
|
529
|
+
AffinityKey: tmpRelPath,
|
|
530
|
+
TimeoutMs: 180000
|
|
531
|
+
},
|
|
532
|
+
(pDispatchError, pResult) =>
|
|
533
|
+
{
|
|
534
|
+
if (!pDispatchError && pResult && pResult.OutputBuffer)
|
|
535
|
+
{
|
|
536
|
+
try
|
|
537
|
+
{
|
|
538
|
+
let tmpDir = libPath.dirname(pOutputPath);
|
|
539
|
+
if (!libFs.existsSync(tmpDir))
|
|
540
|
+
{
|
|
541
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
542
|
+
}
|
|
543
|
+
libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
|
|
544
|
+
tmpSelf.fable.log.info(`Raw conversion via Ultravisor (ImageMagick) for ${tmpRelPath}`);
|
|
545
|
+
return fCallback(null);
|
|
546
|
+
}
|
|
547
|
+
catch (pWriteError)
|
|
548
|
+
{
|
|
549
|
+
// Fall through to local
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Fall through to local processing
|
|
554
|
+
tmpSelf._convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback);
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return this._convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Convert a raw image to JPEG locally using ImageMagick's convert command.
|
|
565
|
+
*/
|
|
566
|
+
_convertWithImageMagickLocal(pAbsPath, pOutputPath, fCallback)
|
|
423
567
|
{
|
|
424
568
|
if (!this._capabilities.imagemagick)
|
|
425
569
|
{
|