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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Remote -- Ebook Conversion Service
|
|
3
|
+
*
|
|
4
|
+
* Converts MOBI/AZW/KF8 ebooks to EPUB using Calibre's ebook-convert tool.
|
|
5
|
+
* Conversions are cached so repeated requests are instant.
|
|
6
|
+
*
|
|
7
|
+
* API:
|
|
8
|
+
* convertToEpub(pAbsPath, pRelPath, fCallback)
|
|
9
|
+
* -> { Success, CacheKey, OutputFilename, SourcePath, FileSize }
|
|
10
|
+
*
|
|
11
|
+
* @license MIT
|
|
12
|
+
*/
|
|
13
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
14
|
+
const libFs = require('fs');
|
|
15
|
+
const libPath = require('path');
|
|
16
|
+
const libCrypto = require('crypto');
|
|
17
|
+
const libChildProcess = require('child_process');
|
|
18
|
+
|
|
19
|
+
const _DefaultServiceConfiguration =
|
|
20
|
+
{
|
|
21
|
+
"ContentPath": ".",
|
|
22
|
+
"CachePath": null
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Extensions that can be converted to EPUB by ebook-convert
|
|
26
|
+
const _ConvertibleExtensions =
|
|
27
|
+
{
|
|
28
|
+
'mobi': true,
|
|
29
|
+
'azw': true,
|
|
30
|
+
'azw3': true,
|
|
31
|
+
'kf8': true,
|
|
32
|
+
'kfx': true,
|
|
33
|
+
'fb2': true,
|
|
34
|
+
'lit': true,
|
|
35
|
+
'pdb': true,
|
|
36
|
+
'rtf': true,
|
|
37
|
+
'txt': true,
|
|
38
|
+
'docx': true,
|
|
39
|
+
'odt': true,
|
|
40
|
+
'cbz': true,
|
|
41
|
+
'cbr': true
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
class RetoldRemoteEbookService extends libFableServiceProviderBase
|
|
45
|
+
{
|
|
46
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
47
|
+
{
|
|
48
|
+
super(pFable, pOptions, pServiceHash);
|
|
49
|
+
|
|
50
|
+
this.serviceType = 'RetoldRemoteEbookService';
|
|
51
|
+
|
|
52
|
+
// Merge with defaults
|
|
53
|
+
for (let tmpKey in _DefaultServiceConfiguration)
|
|
54
|
+
{
|
|
55
|
+
if (!(tmpKey in this.options))
|
|
56
|
+
{
|
|
57
|
+
this.options[tmpKey] = _DefaultServiceConfiguration[tmpKey];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
62
|
+
|
|
63
|
+
this.cachePath = this.options.CachePath
|
|
64
|
+
|| libPath.join(process.cwd(), 'dist', 'retold-cache', 'ebook-conversions');
|
|
65
|
+
|
|
66
|
+
// Ensure cache directory exists
|
|
67
|
+
if (!libFs.existsSync(this.cachePath))
|
|
68
|
+
{
|
|
69
|
+
libFs.mkdirSync(this.cachePath, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.fable.log.info(`Ebook Service: cache at ${this.cachePath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a file extension is convertible to EPUB.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} pExtension - Lowercase file extension (no dot)
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
isConvertible(pExtension)
|
|
82
|
+
{
|
|
83
|
+
return !!_ConvertibleExtensions[pExtension];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the cache directory for a specific ebook file.
|
|
88
|
+
* The key is based on the absolute path and modification time,
|
|
89
|
+
* so cache is automatically invalidated when the file changes.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} pAbsPath - Absolute path to the ebook
|
|
92
|
+
* @param {number} pMtimeMs - Modification time in ms
|
|
93
|
+
* @returns {string} Absolute path to the cache directory
|
|
94
|
+
*/
|
|
95
|
+
_getCacheDir(pAbsPath, pMtimeMs)
|
|
96
|
+
{
|
|
97
|
+
let tmpInput = `${pAbsPath}:${pMtimeMs}`;
|
|
98
|
+
let tmpHash = libCrypto.createHash('sha256').update(tmpInput).digest('hex').substring(0, 16);
|
|
99
|
+
return libPath.join(this.cachePath, tmpHash);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert an ebook to EPUB using Calibre's ebook-convert.
|
|
104
|
+
* Results are cached for fast repeated access.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} pAbsPath - Absolute path to the source ebook
|
|
107
|
+
* @param {string} pRelPath - Relative path (for the response)
|
|
108
|
+
* @param {Function} fCallback - Callback(pError, pResult)
|
|
109
|
+
*/
|
|
110
|
+
convertToEpub(pAbsPath, pRelPath, fCallback)
|
|
111
|
+
{
|
|
112
|
+
let tmpSelf = this;
|
|
113
|
+
|
|
114
|
+
// Get file stats for cache key
|
|
115
|
+
let tmpStat;
|
|
116
|
+
try
|
|
117
|
+
{
|
|
118
|
+
tmpStat = libFs.statSync(pAbsPath);
|
|
119
|
+
}
|
|
120
|
+
catch (pError)
|
|
121
|
+
{
|
|
122
|
+
return fCallback(new Error('File not found.'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let tmpCacheDir = this._getCacheDir(pAbsPath, tmpStat.mtimeMs);
|
|
126
|
+
|
|
127
|
+
// Check for cached manifest
|
|
128
|
+
let tmpManifestPath = libPath.join(tmpCacheDir, 'manifest.json');
|
|
129
|
+
if (libFs.existsSync(tmpManifestPath))
|
|
130
|
+
{
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
let tmpManifest = JSON.parse(libFs.readFileSync(tmpManifestPath, 'utf8'));
|
|
134
|
+
// Verify the output file still exists
|
|
135
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpManifest.OutputFilename);
|
|
136
|
+
if (libFs.existsSync(tmpOutputPath))
|
|
137
|
+
{
|
|
138
|
+
this.fable.log.info(`Ebook conversion cache hit for ${pRelPath}`);
|
|
139
|
+
return fCallback(null, tmpManifest);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (pError)
|
|
143
|
+
{
|
|
144
|
+
// Corrupted manifest, regenerate
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Ensure cache directory exists
|
|
149
|
+
if (!libFs.existsSync(tmpCacheDir))
|
|
150
|
+
{
|
|
151
|
+
libFs.mkdirSync(tmpCacheDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let tmpOutputFilename = 'converted.epub';
|
|
155
|
+
let tmpOutputPath = libPath.join(tmpCacheDir, tmpOutputFilename);
|
|
156
|
+
|
|
157
|
+
this.fable.log.info(`Converting ebook: ${pRelPath} -> EPUB`);
|
|
158
|
+
|
|
159
|
+
try
|
|
160
|
+
{
|
|
161
|
+
// ebook-convert input.mobi output.epub
|
|
162
|
+
let tmpCmd = `ebook-convert "${pAbsPath}" "${tmpOutputPath}"`;
|
|
163
|
+
libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 120000 });
|
|
164
|
+
|
|
165
|
+
if (!libFs.existsSync(tmpOutputPath))
|
|
166
|
+
{
|
|
167
|
+
return fCallback(new Error('Conversion completed but output file not found.'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let tmpOutputStat = libFs.statSync(tmpOutputPath);
|
|
171
|
+
|
|
172
|
+
let tmpResult =
|
|
173
|
+
{
|
|
174
|
+
Success: true,
|
|
175
|
+
SourcePath: pRelPath,
|
|
176
|
+
CacheKey: libPath.basename(tmpCacheDir),
|
|
177
|
+
OutputFilename: tmpOutputFilename,
|
|
178
|
+
FileSize: tmpOutputStat.size,
|
|
179
|
+
ConvertedAt: new Date().toISOString()
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Write manifest to cache
|
|
183
|
+
try
|
|
184
|
+
{
|
|
185
|
+
libFs.writeFileSync(tmpManifestPath, JSON.stringify(tmpResult, null, '\t'));
|
|
186
|
+
}
|
|
187
|
+
catch (pWriteError)
|
|
188
|
+
{
|
|
189
|
+
tmpSelf.fable.log.warn(`Could not write ebook manifest: ${pWriteError.message}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
tmpSelf.fable.log.info(`Converted ebook: ${pRelPath} (${tmpOutputStat.size} bytes)`);
|
|
193
|
+
return fCallback(null, tmpResult);
|
|
194
|
+
}
|
|
195
|
+
catch (pError)
|
|
196
|
+
{
|
|
197
|
+
return fCallback(new Error('Ebook conversion failed: ' + pError.message));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the absolute path to a cached converted ebook file.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} pCacheKey - The cache key (directory name)
|
|
205
|
+
* @param {string} pFilename - The output filename
|
|
206
|
+
* @returns {string|null} Absolute path or null if not found
|
|
207
|
+
*/
|
|
208
|
+
getConvertedPath(pCacheKey, pFilename)
|
|
209
|
+
{
|
|
210
|
+
// Sanitize inputs to prevent directory traversal
|
|
211
|
+
if (!pCacheKey || !pFilename)
|
|
212
|
+
{
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (pCacheKey.includes('..') || pCacheKey.includes('/') || pCacheKey.includes('\\'))
|
|
216
|
+
{
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (pFilename.includes('..') || pFilename.includes('/') || pFilename.includes('\\'))
|
|
220
|
+
{
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let tmpPath = libPath.join(this.cachePath, pCacheKey, pFilename);
|
|
225
|
+
|
|
226
|
+
// Double-check it's under our cache dir
|
|
227
|
+
let tmpResolved = libPath.resolve(tmpPath);
|
|
228
|
+
if (!tmpResolved.startsWith(this.cachePath))
|
|
229
|
+
{
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (libFs.existsSync(tmpPath))
|
|
234
|
+
{
|
|
235
|
+
return tmpPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = RetoldRemoteEbookService;
|
|
@@ -52,7 +52,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
|
|
|
52
52
|
this.contentPath = libPath.resolve(this.options.ContentPath);
|
|
53
53
|
|
|
54
54
|
let tmpCachePath = this.options.ThumbnailCachePath
|
|
55
|
-
|| libPath.join(
|
|
55
|
+
|| libPath.join(process.cwd(), 'dist', 'retold-cache', 'thumbnails');
|
|
56
56
|
|
|
57
57
|
this.toolDetector = new libToolDetector();
|
|
58
58
|
this.capabilities = this.toolDetector.detect();
|
|
@@ -31,7 +31,11 @@ class ToolDetector
|
|
|
31
31
|
sharp: this._detectSharp(),
|
|
32
32
|
imagemagick: this._detectCommand('identify --version'),
|
|
33
33
|
ffmpeg: this._detectCommand('ffmpeg -version'),
|
|
34
|
-
ffprobe: this._detectCommand('ffprobe -version')
|
|
34
|
+
ffprobe: this._detectCommand('ffprobe -version'),
|
|
35
|
+
vlc: this._detectVLC(),
|
|
36
|
+
p7zip: this._detectCommand('7z --help'),
|
|
37
|
+
audiowaveform: this._detectCommand('audiowaveform --version'),
|
|
38
|
+
ebook_convert: this._detectCommand('ebook-convert --version')
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
return this._capabilities;
|
|
@@ -55,6 +59,32 @@ class ToolDetector
|
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Check if VLC is available. On macOS, check for the .app bundle.
|
|
64
|
+
* On Linux, check the vlc command.
|
|
65
|
+
*
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
_detectVLC()
|
|
69
|
+
{
|
|
70
|
+
// macOS: check for VLC.app
|
|
71
|
+
try
|
|
72
|
+
{
|
|
73
|
+
const libFS = require('fs');
|
|
74
|
+
if (libFS.existsSync('/Applications/VLC.app'))
|
|
75
|
+
{
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (pError)
|
|
80
|
+
{
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Linux / other: try vlc --version
|
|
85
|
+
return this._detectCommand('vlc --version');
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
/**
|
|
59
89
|
* Check if a command-line tool is available by running it.
|
|
60
90
|
*
|