retold-remote 0.0.5 → 0.0.7

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 (71) hide show
  1. package/css/retold-remote.css +3 -0
  2. package/docs/README.md +181 -0
  3. package/docs/_cover.md +14 -0
  4. package/docs/_sidebar.md +10 -0
  5. package/docs/_topbar.md +3 -0
  6. package/docs/audio-viewer.md +133 -0
  7. package/docs/ebook-reader.md +90 -0
  8. package/docs/image-viewer.md +90 -0
  9. package/docs/server-setup.md +262 -0
  10. package/docs/video-viewer.md +134 -0
  11. package/html/docs.html +59 -0
  12. package/html/index.html +1 -1
  13. package/package.json +20 -6
  14. package/source/Pict-Application-RetoldRemote.js +122 -2
  15. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  16. package/source/cli/RetoldRemote-Server-Setup.js +211 -67
  17. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  18. package/source/providers/Pict-Provider-AISortManager.js +456 -0
  19. package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
  20. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  21. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  22. package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
  23. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  24. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  27. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  28. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  29. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  30. package/source/server/RetoldRemote-AISortService.js +879 -0
  31. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  32. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  33. package/source/server/RetoldRemote-CollectionService.js +843 -0
  34. package/source/server/RetoldRemote-EbookService.js +7 -16
  35. package/source/server/RetoldRemote-FileOperationService.js +560 -0
  36. package/source/server/RetoldRemote-MediaService.js +15 -14
  37. package/source/server/RetoldRemote-MetadataCache.js +411 -0
  38. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  39. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  40. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  41. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  42. package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
  43. package/source/views/PictView-Remote-Gallery.js +237 -44
  44. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  45. package/source/views/PictView-Remote-Layout.js +411 -20
  46. package/source/views/PictView-Remote-MediaViewer.js +311 -28
  47. package/source/views/PictView-Remote-SettingsPanel.js +275 -133
  48. package/source/views/PictView-Remote-TopBar.js +701 -14
  49. package/source/views/PictView-Remote-VLCSetup.js +8 -39
  50. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  51. package/web-application/css/docuserve.css +73 -0
  52. package/web-application/css/retold-remote.css +3 -0
  53. package/web-application/docs/README.md +181 -0
  54. package/web-application/docs/_cover.md +14 -0
  55. package/web-application/docs/_sidebar.md +10 -0
  56. package/web-application/docs/_topbar.md +3 -0
  57. package/web-application/docs/audio-viewer.md +133 -0
  58. package/web-application/docs/ebook-reader.md +90 -0
  59. package/web-application/docs/image-viewer.md +90 -0
  60. package/web-application/docs/server-setup.md +262 -0
  61. package/web-application/docs/video-viewer.md +134 -0
  62. package/web-application/docs.html +59 -0
  63. package/web-application/index.html +1 -1
  64. package/web-application/js/pict-docuserve.min.js +58 -0
  65. package/web-application/js/pict.min.js +2 -2
  66. package/web-application/js/pict.min.js.map +1 -1
  67. package/web-application/retold-remote.js +2652 -970
  68. package/web-application/retold-remote.js.map +1 -1
  69. package/web-application/retold-remote.min.js +60 -13
  70. package/web-application/retold-remote.min.js.map +1 -1
  71. package/server.js +0 -43
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Retold Remote -- AI Sort Manager Provider
3
+ *
4
+ * Client-side state management and API communication for the
5
+ * AI-powered file sorting feature. Manages AI endpoint settings,
6
+ * connection testing, folder scanning, and sort plan generation.
7
+ *
8
+ * Sort plans are created as operation-plan collections, so all
9
+ * preview, editing, and execution happens through the existing
10
+ * collections infrastructure.
11
+ *
12
+ * Settings are persisted via localStorage alongside other RetoldRemote
13
+ * settings.
14
+ *
15
+ * @license MIT
16
+ */
17
+ const libPictProvider = require('pict-provider');
18
+
19
+ const _DefaultProviderConfiguration =
20
+ {
21
+ ProviderIdentifier: 'RetoldRemote-AISortManager',
22
+ AutoInitialize: true,
23
+ AutoSolveWithApp: false
24
+ };
25
+
26
+ class AISortManagerProvider extends libPictProvider
27
+ {
28
+ constructor(pFable, pOptions, pServiceHash)
29
+ {
30
+ super(pFable, pOptions, pServiceHash);
31
+
32
+ // Track in-progress operations
33
+ this._generating = false;
34
+ this._scanning = false;
35
+ this._testingConnection = false;
36
+ }
37
+
38
+ // -- State Accessors --------------------------------------------------
39
+
40
+ /**
41
+ * Shortcut to the RetoldRemote AppData namespace.
42
+ */
43
+ _getRemote()
44
+ {
45
+ return this.pict.AppData.RetoldRemote;
46
+ }
47
+
48
+ /**
49
+ * Get the AI sort settings from AppData.
50
+ */
51
+ _getSettings()
52
+ {
53
+ let tmpRemote = this._getRemote();
54
+ if (!tmpRemote.AISortSettings)
55
+ {
56
+ tmpRemote.AISortSettings =
57
+ {
58
+ AIEndpoint: 'http://localhost:11434',
59
+ AIModel: 'llama3.1',
60
+ AIProvider: 'ollama',
61
+ NamingTemplate: '{artist}/{album}/{track} - {title}'
62
+ };
63
+ }
64
+ return tmpRemote.AISortSettings;
65
+ }
66
+
67
+ /**
68
+ * Get the toast notification provider.
69
+ */
70
+ _getToast()
71
+ {
72
+ return this.pict.providers['RetoldRemote-ToastNotification'];
73
+ }
74
+
75
+ /**
76
+ * Get the collection manager provider.
77
+ */
78
+ _getCollectionManager()
79
+ {
80
+ return this.pict.providers['RetoldRemote-CollectionManager'];
81
+ }
82
+
83
+ // -- Settings Management ----------------------------------------------
84
+
85
+ /**
86
+ * Update AI sort settings.
87
+ *
88
+ * @param {object} pSettings - Settings object with any of: AIEndpoint, AIModel, AIProvider, NamingTemplate
89
+ */
90
+ updateSettings(pSettings)
91
+ {
92
+ let tmpSettings = this._getSettings();
93
+
94
+ if (pSettings.AIEndpoint !== undefined)
95
+ {
96
+ tmpSettings.AIEndpoint = pSettings.AIEndpoint;
97
+ }
98
+ if (pSettings.AIModel !== undefined)
99
+ {
100
+ tmpSettings.AIModel = pSettings.AIModel;
101
+ }
102
+ if (pSettings.AIProvider !== undefined)
103
+ {
104
+ tmpSettings.AIProvider = pSettings.AIProvider;
105
+ }
106
+ if (pSettings.NamingTemplate !== undefined)
107
+ {
108
+ tmpSettings.NamingTemplate = pSettings.NamingTemplate;
109
+ }
110
+
111
+ // Persist settings
112
+ this.pict.PictApplication.saveSettings();
113
+ }
114
+
115
+ // -- API Methods ------------------------------------------------------
116
+
117
+ /**
118
+ * Test the AI endpoint connection.
119
+ *
120
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
121
+ */
122
+ testConnection(fCallback)
123
+ {
124
+ let tmpSelf = this;
125
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
126
+
127
+ if (this._testingConnection)
128
+ {
129
+ return tmpCallback(new Error('Connection test already in progress'));
130
+ }
131
+
132
+ this._testingConnection = true;
133
+ let tmpSettings = this._getSettings();
134
+
135
+ let tmpToast = this._getToast();
136
+
137
+ fetch('/api/ai-sort/test-connection',
138
+ {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify(
142
+ {
143
+ AIEndpoint: tmpSettings.AIEndpoint,
144
+ AIModel: tmpSettings.AIModel,
145
+ AIProvider: tmpSettings.AIProvider
146
+ })
147
+ })
148
+ .then((pResponse) => pResponse.json())
149
+ .then((pData) =>
150
+ {
151
+ tmpSelf._testingConnection = false;
152
+
153
+ if (pData.Success)
154
+ {
155
+ if (tmpToast)
156
+ {
157
+ tmpToast.show('AI connected (' + pData.ResponseTime + 'ms)');
158
+ }
159
+ }
160
+ else
161
+ {
162
+ if (tmpToast)
163
+ {
164
+ tmpToast.show('AI connection failed: ' + (pData.Error || 'Unknown error'));
165
+ }
166
+ }
167
+
168
+ // Update the test button state if settings panel is open
169
+ tmpSelf._updateTestButtonState(pData.Success);
170
+
171
+ return tmpCallback(null, pData);
172
+ })
173
+ .catch((pError) =>
174
+ {
175
+ tmpSelf._testingConnection = false;
176
+
177
+ if (tmpToast)
178
+ {
179
+ tmpToast.show('AI connection error: ' + pError.message);
180
+ }
181
+
182
+ tmpSelf._updateTestButtonState(false);
183
+
184
+ return tmpCallback(pError);
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Scan a folder for audio files and return metadata.
190
+ *
191
+ * @param {string} pPath - Folder path relative to content root
192
+ * @param {boolean} [pRecursive] - Scan subdirectories
193
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
194
+ */
195
+ scanFolder(pPath, pRecursive, fCallback)
196
+ {
197
+ let tmpSelf = this;
198
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
199
+
200
+ if (this._scanning)
201
+ {
202
+ return tmpCallback(new Error('Scan already in progress'));
203
+ }
204
+
205
+ this._scanning = true;
206
+
207
+ fetch('/api/ai-sort/scan',
208
+ {
209
+ method: 'POST',
210
+ headers: { 'Content-Type': 'application/json' },
211
+ body: JSON.stringify(
212
+ {
213
+ Path: pPath || '',
214
+ Recursive: pRecursive || false
215
+ })
216
+ })
217
+ .then((pResponse) => pResponse.json())
218
+ .then((pData) =>
219
+ {
220
+ tmpSelf._scanning = false;
221
+
222
+ let tmpToast = tmpSelf._getToast();
223
+ if (tmpToast)
224
+ {
225
+ if (pData.Success)
226
+ {
227
+ tmpToast.show('Scanned ' + pData.FileCount + ' audio files');
228
+ }
229
+ else
230
+ {
231
+ tmpToast.show('Scan failed: ' + (pData.Error || 'Unknown error'));
232
+ }
233
+ }
234
+
235
+ return tmpCallback(null, pData);
236
+ })
237
+ .catch((pError) =>
238
+ {
239
+ tmpSelf._scanning = false;
240
+
241
+ let tmpToast = tmpSelf._getToast();
242
+ if (tmpToast)
243
+ {
244
+ tmpToast.show('Scan error: ' + pError.message);
245
+ }
246
+
247
+ return tmpCallback(pError);
248
+ });
249
+ }
250
+
251
+ /**
252
+ * Generate a sort plan for a folder.
253
+ *
254
+ * Creates an operation-plan collection and opens it in the
255
+ * collections panel for preview and editing.
256
+ *
257
+ * @param {string} pPath - Folder path relative to content root
258
+ * @param {Function} [fCallback] - Optional callback(pError, pResult)
259
+ */
260
+ generateSortPlan(pPath, fCallback)
261
+ {
262
+ let tmpSelf = this;
263
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
264
+
265
+ if (this._generating)
266
+ {
267
+ let tmpToast = this._getToast();
268
+ if (tmpToast)
269
+ {
270
+ tmpToast.show('Sort plan generation already in progress...');
271
+ }
272
+ return tmpCallback(new Error('Generation already in progress'));
273
+ }
274
+
275
+ this._generating = true;
276
+ let tmpSettings = this._getSettings();
277
+
278
+ let tmpToast = this._getToast();
279
+ if (tmpToast)
280
+ {
281
+ tmpToast.show('Generating sort plan...');
282
+ }
283
+
284
+ fetch('/api/ai-sort/generate-plan',
285
+ {
286
+ method: 'POST',
287
+ headers: { 'Content-Type': 'application/json' },
288
+ body: JSON.stringify(
289
+ {
290
+ Path: pPath || '',
291
+ NamingTemplate: tmpSettings.NamingTemplate,
292
+ Recursive: true,
293
+ AIEndpoint: tmpSettings.AIEndpoint,
294
+ AIModel: tmpSettings.AIModel,
295
+ AIProvider: tmpSettings.AIProvider
296
+ })
297
+ })
298
+ .then((pResponse) => pResponse.json())
299
+ .then((pData) =>
300
+ {
301
+ tmpSelf._generating = false;
302
+
303
+ if (!pData.Success)
304
+ {
305
+ if (tmpToast)
306
+ {
307
+ tmpToast.show('Sort plan failed: ' + (pData.Error || pData.Message || 'Unknown error'));
308
+ }
309
+ return tmpCallback(new Error(pData.Error || pData.Message));
310
+ }
311
+
312
+ if (!pData.CollectionGUID)
313
+ {
314
+ if (tmpToast)
315
+ {
316
+ tmpToast.show(pData.Message || 'No audio files found');
317
+ }
318
+ return tmpCallback(null, pData);
319
+ }
320
+
321
+ // Open the generated collection in the panel
322
+ let tmpCollManager = tmpSelf._getCollectionManager();
323
+ if (tmpCollManager)
324
+ {
325
+ let tmpRemote = tmpSelf._getRemote();
326
+ tmpRemote.CollectionsPanelMode = 'detail';
327
+
328
+ if (!tmpRemote.CollectionsPanelOpen)
329
+ {
330
+ tmpCollManager.togglePanel();
331
+ }
332
+
333
+ tmpCollManager.fetchCollection(pData.CollectionGUID);
334
+ tmpCollManager.fetchCollections();
335
+ }
336
+
337
+ if (tmpToast)
338
+ {
339
+ let tmpMsg = 'Sort plan created: ' + pData.TotalFiles + ' files';
340
+ if (pData.TaggedFiles > 0)
341
+ {
342
+ tmpMsg += ' (' + pData.TaggedFiles + ' by tags';
343
+ if (pData.AIFiles > 0)
344
+ {
345
+ tmpMsg += ', ' + pData.AIFiles + ' by AI';
346
+ }
347
+ tmpMsg += ')';
348
+ }
349
+ tmpToast.show(tmpMsg);
350
+ }
351
+
352
+ return tmpCallback(null, pData);
353
+ })
354
+ .catch((pError) =>
355
+ {
356
+ tmpSelf._generating = false;
357
+
358
+ if (tmpToast)
359
+ {
360
+ tmpToast.show('Sort plan error: ' + pError.message);
361
+ }
362
+
363
+ return tmpCallback(pError);
364
+ });
365
+ }
366
+
367
+ // -- UI Helpers --------------------------------------------------------
368
+
369
+ /**
370
+ * Update the test connection button state in the settings panel.
371
+ *
372
+ * @param {boolean} pSuccess - Whether the test was successful
373
+ */
374
+ _updateTestButtonState(pSuccess)
375
+ {
376
+ let tmpBtn = document.getElementById('RetoldRemote-AISortTestBtn');
377
+ if (!tmpBtn)
378
+ {
379
+ return;
380
+ }
381
+
382
+ tmpBtn.disabled = false;
383
+
384
+ if (pSuccess === true)
385
+ {
386
+ tmpBtn.style.borderColor = 'var(--retold-accent)';
387
+ tmpBtn.style.color = 'var(--retold-accent)';
388
+ tmpBtn.textContent = 'Connected';
389
+ setTimeout(() =>
390
+ {
391
+ tmpBtn.textContent = 'Test Connection';
392
+ tmpBtn.style.borderColor = '';
393
+ tmpBtn.style.color = '';
394
+ }, 3000);
395
+ }
396
+ else if (pSuccess === false)
397
+ {
398
+ tmpBtn.style.borderColor = 'var(--retold-danger-muted)';
399
+ tmpBtn.style.color = 'var(--retold-danger-muted)';
400
+ tmpBtn.textContent = 'Failed';
401
+ setTimeout(() =>
402
+ {
403
+ tmpBtn.textContent = 'Test Connection';
404
+ tmpBtn.style.borderColor = '';
405
+ tmpBtn.style.color = '';
406
+ }, 3000);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Get a preview of the naming template with sample data.
412
+ *
413
+ * @param {string} pTemplate - Naming template
414
+ * @returns {string} Preview string
415
+ */
416
+ getTemplatePreview(pTemplate)
417
+ {
418
+ let tmpTemplate = pTemplate || '{artist}/{album}/{track} - {title}';
419
+ let tmpPreview = tmpTemplate
420
+ .replace(/\{artist\}/gi, 'Pink Floyd')
421
+ .replace(/\{album\}/gi, 'Dark Side of the Moon')
422
+ .replace(/\{title\}/gi, 'Time')
423
+ .replace(/\{track\}/gi, '04')
424
+ .replace(/\{year\}/gi, '1973')
425
+ .replace(/\{genre\}/gi, 'Progressive Rock');
426
+
427
+ return tmpPreview + '.mp3';
428
+ }
429
+
430
+ /**
431
+ * Whether the AI sort button should be visible for the current folder.
432
+ *
433
+ * Returns true if we're in gallery mode (browsing a folder).
434
+ *
435
+ * @returns {boolean}
436
+ */
437
+ isAvailable()
438
+ {
439
+ let tmpRemote = this._getRemote();
440
+ return tmpRemote.ActiveMode === 'gallery';
441
+ }
442
+
443
+ /**
444
+ * Whether a generation is currently in progress.
445
+ *
446
+ * @returns {boolean}
447
+ */
448
+ isGenerating()
449
+ {
450
+ return this._generating;
451
+ }
452
+ }
453
+
454
+ AISortManagerProvider.default_configuration = _DefaultProviderConfiguration;
455
+
456
+ module.exports = AISortManagerProvider;