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.
Files changed (33) hide show
  1. package/html/index.html +2 -0
  2. package/package.json +20 -14
  3. package/source/Pict-Application-RetoldRemote.js +46 -5
  4. package/source/cli/RetoldRemote-CLI-Run.js +0 -0
  5. package/source/cli/RetoldRemote-Server-Setup.js +790 -8
  6. package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
  7. package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
  8. package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
  9. package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
  10. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
  11. package/source/server/RetoldRemote-ArchiveService.js +830 -0
  12. package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
  13. package/source/server/RetoldRemote-EbookService.js +242 -0
  14. package/source/server/RetoldRemote-MediaService.js +1 -1
  15. package/source/server/RetoldRemote-ToolDetector.js +31 -1
  16. package/source/server/RetoldRemote-VideoFrameService.js +486 -0
  17. package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
  18. package/source/views/PictView-Remote-Gallery.js +141 -2
  19. package/source/views/PictView-Remote-Layout.js +18 -27
  20. package/source/views/PictView-Remote-MediaViewer.js +638 -39
  21. package/source/views/PictView-Remote-SettingsPanel.js +23 -0
  22. package/source/views/PictView-Remote-TopBar.js +121 -0
  23. package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
  24. package/web-application/index.html +2 -0
  25. package/web-application/js/epub.min.js +1 -0
  26. package/web-application/retold-remote.js +7030 -1244
  27. package/web-application/retold-remote.js.map +1 -1
  28. package/web-application/retold-remote.min.js +13 -44
  29. package/web-application/retold-remote.min.js.map +1 -1
  30. package/web-application/retold-remote.compatible.js +0 -5764
  31. package/web-application/retold-remote.compatible.js.map +0 -1
  32. package/web-application/retold-remote.compatible.min.js +0 -120
  33. 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(this.contentPath, '.retold-remote-cache');
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
  *