retold-remote 0.0.23 → 0.0.25
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 +87 -20
- package/docs/README.md +59 -11
- package/docs/_sidebar.md +1 -0
- package/docs/collections.md +30 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +27 -1
- package/docs/server-setup.md +28 -18
- package/docs/stack-launcher.md +218 -0
- package/docs/ultravisor-integration.md +2 -0
- package/package.json +10 -7
- package/source/Pict-Application-RetoldRemote.js +2 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +240 -2
- package/source/cli/RetoldRemote-Stack-Launcher.js +387 -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 +46 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-CollectionExportService.js +696 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +194 -3
- package/source/server/RetoldRemote-SubimageService.js +530 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +963 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
- package/source/views/PictView-Remote-ImageExplorer.js +606 -1
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +12 -0
- package/source/views/PictView-Remote-MediaViewer.js +83 -25
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/web-application/css/retold-remote.css +87 -20
- package/web-application/docs/README.md +59 -11
- package/web-application/docs/_sidebar.md +1 -0
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +27 -1
- package/web-application/docs/server-setup.md +28 -18
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/ultravisor-integration.md +2 -0
- package/web-application/retold-remote.js +399 -45
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -12
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Stack Launcher
|
|
2
|
+
|
|
3
|
+
The `--stack` flag (and the `retold-stack` shortcut) brings up the full Retold stack as a single command: an embedded Ultravisor coordinator, the Retold Remote media browser, and Orator-Conversion (embedded inside Retold Remote). Point it at any directory and it just works — sane XDG-style data paths, automatic readiness polling, and graceful shutdown of all child processes.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run the full stack against the current directory
|
|
9
|
+
retold-stack
|
|
10
|
+
|
|
11
|
+
# Or against any directory
|
|
12
|
+
retold-stack /mnt/nas/media
|
|
13
|
+
|
|
14
|
+
# Or via the explicit flag
|
|
15
|
+
retold-remote serve --stack /mnt/nas/media
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's it. No config files, no separate terminals, no manual port wiring.
|
|
19
|
+
|
|
20
|
+
## What Gets Launched
|
|
21
|
+
|
|
22
|
+
| Component | Where it runs | Port |
|
|
23
|
+
|-----------|---------------|------|
|
|
24
|
+
| **Ultravisor** | Spawned as a child process | 54321 |
|
|
25
|
+
| **Retold Remote** | Main process (the one you started) | Random 7000-7999 (override with `-p`) |
|
|
26
|
+
| **Orator-Conversion** | Embedded inside Retold Remote (no separate process) | Same as Retold Remote |
|
|
27
|
+
|
|
28
|
+
The stack launcher:
|
|
29
|
+
1. Resolves XDG-style data paths
|
|
30
|
+
2. Detects whether Ultravisor is already running on port 54321 (and reuses it if so)
|
|
31
|
+
3. Spawns Ultravisor as a child process with a generated config file
|
|
32
|
+
4. Polls until Ultravisor is accepting connections (up to 30 seconds)
|
|
33
|
+
5. Sets the `UltravisorURL` automatically so Retold Remote registers as a beacon
|
|
34
|
+
6. Starts the Retold Remote server with Orator-Conversion embedded
|
|
35
|
+
7. Streams the Ultravisor child's stdout/stderr through the main logger with an `[ultravisor]` prefix
|
|
36
|
+
8. On `SIGINT`/`SIGTERM`, disconnects the beacon, kills the Ultravisor child gracefully, and exits
|
|
37
|
+
|
|
38
|
+
## Default Data Paths
|
|
39
|
+
|
|
40
|
+
The stack launcher uses [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) defaults so data lives in predictable, user-scoped locations regardless of what directory you launched from.
|
|
41
|
+
|
|
42
|
+
| Purpose | Default Path | Override |
|
|
43
|
+
|---------|-------------|----------|
|
|
44
|
+
| Ultravisor datastore | `~/.local/share/ultravisor/datastore/` | `$XDG_DATA_HOME/ultravisor/datastore/` |
|
|
45
|
+
| Ultravisor staging | `~/.local/share/ultravisor/staging/` | `$XDG_DATA_HOME/ultravisor/staging/` |
|
|
46
|
+
| Retold Remote cache | `~/.cache/retold-remote/` | `$XDG_CACHE_HOME/retold-remote/`, or `-c` flag |
|
|
47
|
+
| Stack config files | `~/.config/retold-stack/` | `$XDG_CONFIG_HOME/retold-stack/` |
|
|
48
|
+
|
|
49
|
+
The cache directory holds thumbnails, video frames, audio waveforms, archive extractions, and converted ebooks/PDFs.
|
|
50
|
+
|
|
51
|
+
The Ultravisor datastore holds the work-queue journal and beacon registry.
|
|
52
|
+
|
|
53
|
+
These paths are created on first launch if they do not already exist.
|
|
54
|
+
|
|
55
|
+
## CLI Reference
|
|
56
|
+
|
|
57
|
+
### `retold-stack`
|
|
58
|
+
|
|
59
|
+
Convenience entry point that auto-injects `serve --stack`.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
retold-stack [content-path] [options]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All options accepted by `retold-remote serve` are forwarded.
|
|
66
|
+
|
|
67
|
+
### `retold-remote serve --stack`
|
|
68
|
+
|
|
69
|
+
The full form. Useful when you want to combine `--stack` with other flags.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
retold-remote serve --stack [content-path] [options]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Flag | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `--stack` | Spawn Ultravisor as a child process and connect to it. Sets cache root to `~/.cache/retold-remote/` if `-c` is not passed. Auto-sets the Ultravisor URL to `http://localhost:54321`. |
|
|
78
|
+
|
|
79
|
+
All other `serve` options still apply (`-p`, `--no-hash`, `-c`, `-l`, etc.).
|
|
80
|
+
|
|
81
|
+
## Examples
|
|
82
|
+
|
|
83
|
+
### Browse the current directory with the full stack
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd ~/Pictures
|
|
87
|
+
retold-stack
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Browse a specific NAS share
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
retold-stack /mnt/nas/media
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Pin the port and stash logs to a file
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
retold-stack /mnt/nas/media -p 8086 -l ~/retold.log
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Override just the Retold cache location
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
retold-remote serve --stack /mnt/nas/media -c /var/cache/retold
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Reuse an already-running Ultravisor
|
|
109
|
+
|
|
110
|
+
If port 54321 is already accepting connections, the stack launcher detects this and connects to the existing instance instead of spawning a new one. The log line will read:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
[stack] ultravisor already running on port 54321, reusing
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This means you can leave Ultravisor running across multiple Retold Remote launches without conflict.
|
|
117
|
+
|
|
118
|
+
## Logging
|
|
119
|
+
|
|
120
|
+
The stack launcher streams the Ultravisor child process's output through Retold Remote's logger so you have a single log stream for both processes. Lines from the Ultravisor child are prefixed `[ultravisor]`, and stack-launcher events are prefixed `[stack]`:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
[stack] launching ultravisor (port 54321)
|
|
124
|
+
[stack] data: /Users/steven/.local/share/ultravisor/datastore
|
|
125
|
+
[stack] staging: /Users/steven/.local/share/ultravisor/staging
|
|
126
|
+
[ultravisor] UltravisorTaskTypeRegistry: 53 built-in task types registered.
|
|
127
|
+
[stack] ultravisor ready (after 1 attempts)
|
|
128
|
+
==========================================================
|
|
129
|
+
Retold Remote running on http://localhost:7842
|
|
130
|
+
==========================================================
|
|
131
|
+
Content: /mnt/nas/media
|
|
132
|
+
Cache: /Users/steven/.cache/retold-remote
|
|
133
|
+
Browse: http://localhost:7842/
|
|
134
|
+
Beacon: registered with Ultravisor at http://localhost:54321
|
|
135
|
+
Stack: ultravisor + retold-remote (orator-conversion embedded)
|
|
136
|
+
==========================================================
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If you also pass `-l <path>`, both streams are written to the file as well.
|
|
140
|
+
|
|
141
|
+
## Shutdown
|
|
142
|
+
|
|
143
|
+
Press `Ctrl+C` once. The launcher will:
|
|
144
|
+
|
|
145
|
+
1. Log `Shutting down...`
|
|
146
|
+
2. Disconnect the Ultravisor beacon (so Ultravisor knows the worker is leaving cleanly)
|
|
147
|
+
3. Send `SIGTERM` to the Ultravisor child process
|
|
148
|
+
4. Wait up to 1 second for graceful exit
|
|
149
|
+
5. Send `SIGKILL` if it has not exited
|
|
150
|
+
6. Exit the main process
|
|
151
|
+
|
|
152
|
+
If the launcher reused an already-running Ultravisor (rather than spawning one), it will not kill that Ultravisor — it only manages processes it started itself.
|
|
153
|
+
|
|
154
|
+
## Architecture
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
┌────────────────────────────────────────────────────────────┐
|
|
158
|
+
│ retold-stack /mnt/media │
|
|
159
|
+
│ │
|
|
160
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
161
|
+
│ │ Main process (retold-remote) │ │
|
|
162
|
+
│ │ ┌───────────────────────────────────────────────┐ │ │
|
|
163
|
+
│ │ │ Orator HTTP server (port 7000-7999) │ │ │
|
|
164
|
+
│ │ │ ├─ Retold Remote routes (/api/media/...) │ │ │
|
|
165
|
+
│ │ │ ├─ Orator-Conversion (/api/conversion/1.0/) │ │ │
|
|
166
|
+
│ │ │ └─ Static web app (/) │ │ │
|
|
167
|
+
│ │ └───────────────────────────────────────────────┘ │ │
|
|
168
|
+
│ │ ┌───────────────────────────────────────────────┐ │ │
|
|
169
|
+
│ │ │ Ultravisor Beacon client │ │ │
|
|
170
|
+
│ │ │ └─ Connects to localhost:54321 │ │ │
|
|
171
|
+
│ │ └───────────────────────────────────────────────┘ │ │
|
|
172
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
173
|
+
│ ▲ │
|
|
174
|
+
│ │ HTTP │
|
|
175
|
+
│ ▼ │
|
|
176
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
177
|
+
│ │ Child process (ultravisor) │ │
|
|
178
|
+
│ │ ├─ Coordinator API (port 54321) │ │
|
|
179
|
+
│ │ ├─ Beacon registry │ │
|
|
180
|
+
│ │ ├─ Work queue journal │ │
|
|
181
|
+
│ │ └─ Web interface │ │
|
|
182
|
+
│ │ Datastore: ~/.local/share/ultravisor/datastore/ │ │
|
|
183
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
184
|
+
└────────────────────────────────────────────────────────────┘
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Why Stack Mode?
|
|
188
|
+
|
|
189
|
+
Use `--stack` when you want a single command that brings up the full Retold experience:
|
|
190
|
+
|
|
191
|
+
- **Heavy media processing offloaded** through the Ultravisor beacon (video frame extraction, audio waveforms, ebook conversion, PDF page rendering)
|
|
192
|
+
- **Document conversion** via the embedded Orator-Conversion service (with the `doc-to-pdf` converter for Word, RTF, ODT, WordPerfect, etc.)
|
|
193
|
+
- **Persistent state** across launches (the Ultravisor datastore lives at a stable XDG location)
|
|
194
|
+
- **No port juggling** — Ultravisor on its standard port 54321, Retold Remote on a random high port
|
|
195
|
+
|
|
196
|
+
Use the bare `retold-remote serve` command (without `--stack`) when you just want a quick gallery browser without the coordinator infrastructure.
|
|
197
|
+
|
|
198
|
+
## Comparison
|
|
199
|
+
|
|
200
|
+
| Feature | `retold-remote serve` | `retold-stack` |
|
|
201
|
+
|---------|----------------------|----------------|
|
|
202
|
+
| Gallery browser | ✓ | ✓ |
|
|
203
|
+
| Image/video/audio viewers | ✓ | ✓ |
|
|
204
|
+
| Document conversion (doc, rtf, etc.) | Embedded only | Embedded + dispatched |
|
|
205
|
+
| Heavy work offloading | No | Yes (via Ultravisor) |
|
|
206
|
+
| Spawns child processes | No | Yes (Ultravisor) |
|
|
207
|
+
| Default cache location | `./dist/retold-cache/` | `~/.cache/retold-remote/` |
|
|
208
|
+
| Survives `cd` | Yes | Yes (XDG paths are absolute) |
|
|
209
|
+
|
|
210
|
+
## Troubleshooting
|
|
211
|
+
|
|
212
|
+
| Symptom | Fix |
|
|
213
|
+
|---------|-----|
|
|
214
|
+
| `Could not locate the ultravisor package` | Run `npm install ultravisor` (or `npm install -g retold-remote` to get the bundled version) |
|
|
215
|
+
| `Ultravisor did not become ready within 30000ms` | Check the `[ultravisor]` log lines for an error. Common causes: port 54321 already bound by another process, missing dependencies, or invalid datastore path permissions |
|
|
216
|
+
| Stack mode hangs at `launching ultravisor` | Ultravisor's child process may be waiting on stdin or hitting an interactive prompt. Run with `-l <path>` to capture full logs |
|
|
217
|
+
| Beacon shows "not connected" | Ultravisor came up but the beacon registration failed. Check the Ultravisor child logs for errors and verify nothing is firewalling localhost:54321 |
|
|
218
|
+
| Want to override XDG paths | Set `XDG_DATA_HOME`, `XDG_CACHE_HOME`, or `XDG_CONFIG_HOME` in your environment before launching, or use the `-c` flag for the Retold Remote cache |
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
retold-remote can offload heavy media processing to a remote machine running an Ultravisor beacon worker. This is useful when the server (e.g. a NAS) has limited CPU and RAM but needs to process large video files, raw camera images, audio waveforms, and ebook conversions.
|
|
4
4
|
|
|
5
|
+
> **Tip:** If you just want to run Ultravisor and Retold Remote together on the same machine without configuring anything, use the [Stack Launcher](stack-launcher.md) — `retold-stack /path/to/media` spawns Ultravisor as a child process automatically with sane XDG-style data paths. This page covers the case where Ultravisor runs on a *different* machine.
|
|
6
|
+
|
|
5
7
|
## How It Works
|
|
6
8
|
|
|
7
9
|
When configured, retold-remote dispatches shell commands (ffmpeg, ffprobe, dcraw, ImageMagick, audiowaveform, ebook-convert) to a beacon worker via HTTP instead of running them locally. The beacon downloads the source file from retold-remote's content API, executes the command, and returns the result as base64-encoded data.
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retold-remote",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "Retold Remote - NAS media browser with gallery views and keyboard navigation",
|
|
5
5
|
"main": "source/Pict-RetoldRemote-Bundle.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"retold-remote": "source/cli/RetoldRemote-CLI-Run.js",
|
|
8
|
-
"rr": "source/cli/RetoldRemote-CLI-Run.js"
|
|
8
|
+
"rr": "source/cli/RetoldRemote-CLI-Run.js",
|
|
9
|
+
"retold-stack": "source/cli/RetoldRemote-Stack-Run.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"source/",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"author": "steven velozo <steven@velozo.com>",
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@xmldom/xmldom": "^0.
|
|
34
|
+
"@xmldom/xmldom": "^0.9.9",
|
|
34
35
|
"dcraw": "^1.0.3",
|
|
35
36
|
"epubjs": "^0.3.93",
|
|
36
37
|
"exifr": "^7.1.3",
|
|
@@ -38,10 +39,11 @@
|
|
|
38
39
|
"fable-serviceproviderbase": "^3.0.19",
|
|
39
40
|
"node-unrar-js": "^2.0.2",
|
|
40
41
|
"orator": "^6.0.4",
|
|
41
|
-
"orator-
|
|
42
|
+
"orator-conversion": "^1.0.7",
|
|
43
|
+
"orator-serviceserver-restify": "^2.0.10",
|
|
42
44
|
"parime": "^1.0.3",
|
|
43
45
|
"pdf-parse": "^2.4.5",
|
|
44
|
-
"pict": "^1.0.
|
|
46
|
+
"pict": "^1.0.361",
|
|
45
47
|
"pict-application": "^1.0.33",
|
|
46
48
|
"pict-docuserve": "^0.0.32",
|
|
47
49
|
"pict-provider": "^1.0.12",
|
|
@@ -50,8 +52,9 @@
|
|
|
50
52
|
"pict-service-commandlineutility": "^1.0.19",
|
|
51
53
|
"pict-view": "^1.0.67",
|
|
52
54
|
"retold-content-system": "^1.0.12",
|
|
53
|
-
"ultravisor
|
|
54
|
-
"
|
|
55
|
+
"ultravisor": "^1.0.15",
|
|
56
|
+
"ultravisor-beacon": "^0.0.8",
|
|
57
|
+
"yauzl": "^3.3.0"
|
|
55
58
|
},
|
|
56
59
|
"optionalDependencies": {
|
|
57
60
|
"@img/sharp-wasm32": "^0.34.5",
|
|
@@ -30,6 +30,7 @@ const libViewImageExplorer = require('./views/PictView-Remote-ImageExplorer.js')
|
|
|
30
30
|
const libViewVLCSetup = require('./views/PictView-Remote-VLCSetup.js');
|
|
31
31
|
const libViewCollectionsPanel = require('./views/PictView-Remote-CollectionsPanel.js');
|
|
32
32
|
const libViewFileInfoPanel = require('./views/PictView-Remote-FileInfoPanel.js');
|
|
33
|
+
const libViewSubimagesPanel = require('./views/PictView-Remote-SubimagesPanel.js');
|
|
33
34
|
|
|
34
35
|
// Application configuration
|
|
35
36
|
const _DefaultConfiguration = require('./Pict-Application-RetoldRemote-Configuration.json');
|
|
@@ -65,6 +66,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
|
|
|
65
66
|
this.pict.addView('RetoldRemote-VLCSetup', libViewVLCSetup.default_configuration, libViewVLCSetup);
|
|
66
67
|
this.pict.addView('RetoldRemote-CollectionsPanel', libViewCollectionsPanel.default_configuration, libViewCollectionsPanel);
|
|
67
68
|
this.pict.addView('RetoldRemote-FileInfoPanel', libViewFileInfoPanel.default_configuration, libViewFileInfoPanel);
|
|
69
|
+
this.pict.addView('RetoldRemote-SubimagesPanel', libViewSubimagesPanel.default_configuration, libViewSubimagesPanel);
|
|
68
70
|
|
|
69
71
|
// Add new providers
|
|
70
72
|
this.pict.addProvider('RetoldRemote-Provider', libProviderRetoldRemote.default_configuration, libProviderRetoldRemote);
|
|
@@ -41,7 +41,7 @@ for (let tmpKey in RawImageExtensions)
|
|
|
41
41
|
|
|
42
42
|
const VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true, 'mpg': true, 'mpeg': true, 'mpe': true, 'mpv': true, 'm2v': true, 'ts': true, 'mts': true, 'm2ts': true, 'vob': true, '3gp': true, '3g2': true, 'f4v': true, 'rm': true, 'rmvb': true, 'divx': true, 'asf': true, 'mxf': true, 'dv': true, 'nsv': true, 'nuv': true, 'y4m': true, 'wtv': true, 'swf': true, 'dat': true };
|
|
43
43
|
const AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
|
|
44
|
-
const DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true };
|
|
44
|
+
const DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true, 'rtf': true, 'odt': true, 'wpd': true, 'wps': true, 'pages': true, 'odp': true, 'ppt': true, 'pptx': true, 'ods': true, 'xls': true, 'xlsx': true };
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Get the media category for a file extension.
|
|
@@ -48,8 +48,11 @@ 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 libRetoldRemoteSubimageService = require('../server/RetoldRemote-SubimageService.js');
|
|
52
|
+
const libRetoldRemoteCollectionExportService = require('../server/RetoldRemote-CollectionExportService.js');
|
|
51
53
|
const libRetoldRemoteUltravisorDispatcher = require('../server/RetoldRemote-UltravisorDispatcher.js');
|
|
52
54
|
const libRetoldRemoteUltravisorBeacon = require('../server/RetoldRemote-UltravisorBeacon.js');
|
|
55
|
+
const libOratorConversion = require('orator-conversion');
|
|
53
56
|
const libUrl = require('url');
|
|
54
57
|
|
|
55
58
|
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
@@ -189,6 +192,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
189
192
|
ContentPath: tmpContentPath
|
|
190
193
|
});
|
|
191
194
|
|
|
195
|
+
// Set up the subimage region service
|
|
196
|
+
let tmpSubimageService = new libRetoldRemoteSubimageService(tmpFable,
|
|
197
|
+
{
|
|
198
|
+
ContentPath: tmpContentPath
|
|
199
|
+
});
|
|
200
|
+
|
|
192
201
|
// Set up the metadata cache service
|
|
193
202
|
let tmpMetadataCache = new libRetoldRemoteMetadataCache(tmpFable,
|
|
194
203
|
{
|
|
@@ -212,6 +221,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
212
221
|
let tmpCollectionService = new libRetoldRemoteCollectionService(tmpFable, {});
|
|
213
222
|
tmpCollectionService.setFileOperationService(tmpFileOperationService);
|
|
214
223
|
|
|
224
|
+
// Set up the collection export service
|
|
225
|
+
let tmpCollectionExportService = new libRetoldRemoteCollectionExportService(tmpFable,
|
|
226
|
+
{
|
|
227
|
+
ContentPath: tmpContentPath
|
|
228
|
+
});
|
|
229
|
+
|
|
215
230
|
// Set up the media service
|
|
216
231
|
let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
|
|
217
232
|
{
|
|
@@ -226,11 +241,23 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
226
241
|
// Set up the Ultravisor beacon for mesh registration
|
|
227
242
|
let tmpBeacon = new libRetoldRemoteUltravisorBeacon(tmpFable, {});
|
|
228
243
|
|
|
244
|
+
// Set up orator-conversion for document format conversion
|
|
245
|
+
tmpFable.serviceManager.addServiceType('OratorFileTranslation', libOratorConversion);
|
|
246
|
+
let tmpConversionService = tmpFable.serviceManager.instantiateServiceProvider('OratorFileTranslation',
|
|
247
|
+
{
|
|
248
|
+
RoutePrefix: '/api/conversion',
|
|
249
|
+
MaxFileSize: 100 * 1024 * 1024, // 100MB for large documents
|
|
250
|
+
LogLevel: 1
|
|
251
|
+
});
|
|
252
|
+
|
|
229
253
|
// Wire the dispatcher to services that can offload processing
|
|
230
254
|
tmpMediaService.setDispatcher(tmpDispatcher);
|
|
231
255
|
tmpVideoFrameService.setDispatcher(tmpDispatcher);
|
|
232
256
|
tmpAudioWaveformService.setDispatcher(tmpDispatcher);
|
|
233
257
|
tmpEbookService.setDispatcher(tmpDispatcher);
|
|
258
|
+
|
|
259
|
+
// Share the orator-conversion service with the ebook service for PDF conversion
|
|
260
|
+
tmpEbookService.setConversionService(tmpConversionService);
|
|
234
261
|
tmpImageService.setDispatcher(tmpDispatcher);
|
|
235
262
|
|
|
236
263
|
// Share tool capabilities with the image service so it can
|
|
@@ -238,10 +265,12 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
238
265
|
// Also provides the centrally-verified sharp module reference.
|
|
239
266
|
tmpImageService.setCapabilities(tmpMediaService.capabilities);
|
|
240
267
|
|
|
241
|
-
// Share the verified sharp module with the metadata cache
|
|
268
|
+
// Share the verified sharp module with the metadata cache, subimage, and export services
|
|
242
269
|
if (tmpMediaService.capabilities.sharpModule)
|
|
243
270
|
{
|
|
244
271
|
tmpMetadataCache.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
272
|
+
tmpSubimageService.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
273
|
+
tmpCollectionExportService.setSharpModule(tmpMediaService.capabilities.sharpModule);
|
|
245
274
|
}
|
|
246
275
|
|
|
247
276
|
tmpOrator.initialize(
|
|
@@ -470,6 +499,74 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
470
499
|
// Connect collection service API routes
|
|
471
500
|
tmpCollectionService.connectRoutes(tmpServiceServer);
|
|
472
501
|
|
|
502
|
+
// Connect subimage region service API routes
|
|
503
|
+
tmpSubimageService.connectRoutes(tmpServiceServer);
|
|
504
|
+
|
|
505
|
+
// Connect collection export service API routes
|
|
506
|
+
tmpCollectionExportService.connectRoutes(tmpServiceServer);
|
|
507
|
+
|
|
508
|
+
// Connect orator-conversion routes and register custom doc-to-pdf converter
|
|
509
|
+
if (tmpMediaService.capabilities.libreoffice)
|
|
510
|
+
{
|
|
511
|
+
let tmpSofficePath = 'soffice';
|
|
512
|
+
if (libFs.existsSync('/Applications/LibreOffice.app/Contents/MacOS/soffice'))
|
|
513
|
+
{
|
|
514
|
+
tmpSofficePath = '/Applications/LibreOffice.app/Contents/MacOS/soffice';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
tmpConversionService.addConverter('doc-to-pdf',
|
|
518
|
+
(pInputBuffer, pRequest, fCallback) =>
|
|
519
|
+
{
|
|
520
|
+
// Write input to temp file, convert with LibreOffice, return PDF buffer
|
|
521
|
+
let tmpTempDir = require('os').tmpdir();
|
|
522
|
+
let tmpUniqueId = Date.now() + '_' + Math.random().toString(36).slice(2);
|
|
523
|
+
let tmpInputExt = (pRequest.query && pRequest.query.ext) || 'docx';
|
|
524
|
+
let tmpInputPath = libPath.join(tmpTempDir, 'retold_doc_' + tmpUniqueId + '.' + tmpInputExt);
|
|
525
|
+
let tmpOutputDir = libPath.join(tmpTempDir, 'retold_docout_' + tmpUniqueId);
|
|
526
|
+
|
|
527
|
+
libFs.mkdirSync(tmpOutputDir, { recursive: true });
|
|
528
|
+
|
|
529
|
+
try
|
|
530
|
+
{
|
|
531
|
+
libFs.writeFileSync(tmpInputPath, pInputBuffer);
|
|
532
|
+
libChildProcess.execSync(
|
|
533
|
+
'"' + tmpSofficePath + '" --headless --convert-to pdf --outdir "' + tmpOutputDir + '" "' + tmpInputPath + '"',
|
|
534
|
+
{ stdio: 'ignore', timeout: 120000 });
|
|
535
|
+
|
|
536
|
+
// Find the output PDF (LibreOffice names it after the input)
|
|
537
|
+
let tmpBaseName = libPath.basename(tmpInputPath, '.' + tmpInputExt);
|
|
538
|
+
let tmpOutputPath = libPath.join(tmpOutputDir, tmpBaseName + '.pdf');
|
|
539
|
+
|
|
540
|
+
if (!libFs.existsSync(tmpOutputPath))
|
|
541
|
+
{
|
|
542
|
+
// Clean up
|
|
543
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
544
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
545
|
+
return fCallback(new Error('LibreOffice conversion produced no output.'));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let tmpPdfBuffer = libFs.readFileSync(tmpOutputPath);
|
|
549
|
+
|
|
550
|
+
// Clean up temp files
|
|
551
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
552
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
553
|
+
|
|
554
|
+
return fCallback(null, tmpPdfBuffer, 'application/pdf');
|
|
555
|
+
}
|
|
556
|
+
catch (pError)
|
|
557
|
+
{
|
|
558
|
+
try { libFs.unlinkSync(tmpInputPath); } catch (e) { /* */ }
|
|
559
|
+
try { libFs.rmSync(tmpOutputDir, { recursive: true }); } catch (e) { /* */ }
|
|
560
|
+
return fCallback(new Error('Document conversion failed: ' + pError.message));
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
tmpFable.log.info('Orator-Conversion: doc-to-pdf converter registered (LibreOffice)');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
tmpConversionService.connectRoutes();
|
|
568
|
+
tmpFable.log.info('Orator-Conversion: routes connected at /api/conversion/1.0/');
|
|
569
|
+
|
|
473
570
|
// Connect AI sort service API routes
|
|
474
571
|
tmpAISortService.connectRoutes(tmpServiceServer);
|
|
475
572
|
|
|
@@ -486,6 +583,10 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
486
583
|
{
|
|
487
584
|
tmpFable.log.info('Audio explorer state Bibliograph source initialized.');
|
|
488
585
|
});
|
|
586
|
+
tmpSubimageService.initializeState(() =>
|
|
587
|
+
{
|
|
588
|
+
tmpFable.log.info('Subimage region state Bibliograph source initialized.');
|
|
589
|
+
});
|
|
489
590
|
tmpEpubMetadataService.initialize(() =>
|
|
490
591
|
{
|
|
491
592
|
tmpFable.log.info('EPUB metadata service initialized.');
|
|
@@ -495,6 +596,81 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
495
596
|
// Non-fatal — if Ultravisor is down, processing stays local
|
|
496
597
|
});
|
|
497
598
|
|
|
599
|
+
// --- GET /api/media/pdf-text ---
|
|
600
|
+
// Extract text from a specific PDF page (or all pages if no page specified).
|
|
601
|
+
tmpServiceServer.get('/api/media/pdf-text',
|
|
602
|
+
(pRequest, pResponse, fNext) =>
|
|
603
|
+
{
|
|
604
|
+
try
|
|
605
|
+
{
|
|
606
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
607
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
608
|
+
let tmpRelPath = tmpQuery.path;
|
|
609
|
+
let tmpPageNum = parseInt(tmpQuery.page, 10) || 0;
|
|
610
|
+
|
|
611
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
612
|
+
{
|
|
613
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
614
|
+
return fNext();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
618
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
619
|
+
{
|
|
620
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
621
|
+
return fNext();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
625
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
626
|
+
{
|
|
627
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
628
|
+
return fNext();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
let tmpExt = tmpRelPath.replace(/^.*\./, '').toLowerCase();
|
|
632
|
+
if (tmpExt !== 'pdf')
|
|
633
|
+
{
|
|
634
|
+
pResponse.send(400, { Success: false, Error: 'Not a PDF file.' });
|
|
635
|
+
return fNext();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let tmpPdfParse = require('pdf-parse');
|
|
639
|
+
let tmpBuffer = libFs.readFileSync(tmpAbsPath);
|
|
640
|
+
|
|
641
|
+
let tmpOptions = {};
|
|
642
|
+
if (tmpPageNum > 0)
|
|
643
|
+
{
|
|
644
|
+
// pdf-parse pagerender callback for specific page
|
|
645
|
+
tmpOptions.max = tmpPageNum;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
tmpPdfParse(tmpBuffer, tmpOptions)
|
|
649
|
+
.then((pData) =>
|
|
650
|
+
{
|
|
651
|
+
pResponse.send(
|
|
652
|
+
{
|
|
653
|
+
Success: true,
|
|
654
|
+
Path: tmpRelPath,
|
|
655
|
+
PageCount: pData.numpages,
|
|
656
|
+
Text: pData.text,
|
|
657
|
+
RequestedPage: tmpPageNum || null
|
|
658
|
+
});
|
|
659
|
+
return fNext();
|
|
660
|
+
})
|
|
661
|
+
.catch((pPdfError) =>
|
|
662
|
+
{
|
|
663
|
+
pResponse.send(500, { Success: false, Error: 'PDF parse failed: ' + pPdfError.message });
|
|
664
|
+
return fNext();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch (pError)
|
|
668
|
+
{
|
|
669
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
670
|
+
return fNext();
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
498
674
|
// --- GET /api/media/metadata ---
|
|
499
675
|
// Get cached metadata (with ID3/format tags) for a single file.
|
|
500
676
|
tmpServiceServer.get('/api/media/metadata',
|
|
@@ -1307,9 +1483,17 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1307
1483
|
|
|
1308
1484
|
let tmpStat = libFs.statSync(tmpEbookPath);
|
|
1309
1485
|
|
|
1486
|
+
// Determine content type based on file extension
|
|
1487
|
+
let tmpEbookExt = tmpFilename.replace(/^.*\./, '').toLowerCase();
|
|
1488
|
+
let tmpContentType = 'application/epub+zip';
|
|
1489
|
+
if (tmpEbookExt === 'pdf')
|
|
1490
|
+
{
|
|
1491
|
+
tmpContentType = 'application/pdf';
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1310
1494
|
pResponse.writeHead(200,
|
|
1311
1495
|
{
|
|
1312
|
-
'Content-Type':
|
|
1496
|
+
'Content-Type': tmpContentType,
|
|
1313
1497
|
'Content-Length': tmpStat.size,
|
|
1314
1498
|
'Cache-Control': 'public, max-age=86400'
|
|
1315
1499
|
});
|
|
@@ -1330,6 +1514,57 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1330
1514
|
}
|
|
1331
1515
|
});
|
|
1332
1516
|
|
|
1517
|
+
// --- GET /api/media/doc-convert ---
|
|
1518
|
+
// Convert a document (DOC, DOCX, RTF, ODT, WPD, etc.) to PDF.
|
|
1519
|
+
tmpServiceServer.get('/api/media/doc-convert',
|
|
1520
|
+
(pRequest, pResponse, fNext) =>
|
|
1521
|
+
{
|
|
1522
|
+
try
|
|
1523
|
+
{
|
|
1524
|
+
let tmpParsedUrl = libUrl.parse(pRequest.url, true);
|
|
1525
|
+
let tmpQuery = tmpParsedUrl.query;
|
|
1526
|
+
let tmpRelPath = tmpQuery.path;
|
|
1527
|
+
|
|
1528
|
+
if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
|
|
1529
|
+
{
|
|
1530
|
+
pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
|
|
1531
|
+
return fNext();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
|
|
1535
|
+
if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
|
|
1536
|
+
{
|
|
1537
|
+
pResponse.send(400, { Success: false, Error: 'Invalid path.' });
|
|
1538
|
+
return fNext();
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
let tmpAbsPath = libPath.join(tmpContentPath, tmpRelPath);
|
|
1542
|
+
if (!libFs.existsSync(tmpAbsPath))
|
|
1543
|
+
{
|
|
1544
|
+
pResponse.send(404, { Success: false, Error: 'File not found.' });
|
|
1545
|
+
return fNext();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
tmpEbookService.convertToPdf(tmpAbsPath, tmpRelPath,
|
|
1549
|
+
(pError, pResult) =>
|
|
1550
|
+
{
|
|
1551
|
+
if (pError)
|
|
1552
|
+
{
|
|
1553
|
+
pResponse.send(400, { Success: false, Error: pError.message });
|
|
1554
|
+
return fNext();
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
pResponse.send(pResult);
|
|
1558
|
+
return fNext();
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
catch (pError)
|
|
1562
|
+
{
|
|
1563
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
1564
|
+
return fNext();
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1333
1568
|
// --- GET /api/media/ebook-metadata ---
|
|
1334
1569
|
// Extract and return cached EPUB metadata (TOC, spine, word counts, etc.)
|
|
1335
1570
|
tmpServiceServer.get('/api/media/ebook-metadata',
|
|
@@ -2146,6 +2381,9 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
2146
2381
|
ArchiveService: tmpArchiveService,
|
|
2147
2382
|
VideoFrameService: tmpVideoFrameService,
|
|
2148
2383
|
AudioWaveformService: tmpAudioWaveformService,
|
|
2384
|
+
SubimageService: tmpSubimageService,
|
|
2385
|
+
CollectionExportService: tmpCollectionExportService,
|
|
2386
|
+
ConversionService: tmpConversionService,
|
|
2149
2387
|
PathRegistry: tmpPathRegistry,
|
|
2150
2388
|
ParimeCache: tmpParimeCache,
|
|
2151
2389
|
MetadataCache: tmpMetadataCache,
|