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
@@ -8,6 +8,10 @@ const libProviderGalleryNavigation = require('./providers/Pict-Provider-GalleryN
8
8
  const libProviderGalleryFilterSort = require('./providers/Pict-Provider-GalleryFilterSort.js');
9
9
  const libProviderRetoldRemoteIcons = require('./providers/Pict-Provider-RetoldRemoteIcons.js');
10
10
  const libProviderRetoldRemoteTheme = require('./providers/Pict-Provider-RetoldRemoteTheme.js');
11
+ const libProviderFormattingUtilities = require('./providers/Pict-Provider-FormattingUtilities.js');
12
+ const libProviderToastNotification = require('./providers/Pict-Provider-ToastNotification.js');
13
+ const libProviderCollectionManager = require('./providers/Pict-Provider-CollectionManager.js');
14
+ const libProviderAISortManager = require('./providers/Pict-Provider-AISortManager.js');
11
15
 
12
16
  // Views (replace parent views)
13
17
  const libViewLayout = require('./views/PictView-Remote-Layout.js');
@@ -21,6 +25,7 @@ const libViewImageViewer = require('./views/PictView-Remote-ImageViewer.js');
21
25
  const libViewVideoExplorer = require('./views/PictView-Remote-VideoExplorer.js');
22
26
  const libViewAudioExplorer = require('./views/PictView-Remote-AudioExplorer.js');
23
27
  const libViewVLCSetup = require('./views/PictView-Remote-VLCSetup.js');
28
+ const libViewCollectionsPanel = require('./views/PictView-Remote-CollectionsPanel.js');
24
29
 
25
30
  // Application configuration
26
31
  const _DefaultConfiguration = require('./Pict-Application-RetoldRemote-Configuration.json');
@@ -53,6 +58,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
53
58
  this.pict.addView('RetoldRemote-VideoExplorer', libViewVideoExplorer.default_configuration, libViewVideoExplorer);
54
59
  this.pict.addView('RetoldRemote-AudioExplorer', libViewAudioExplorer.default_configuration, libViewAudioExplorer);
55
60
  this.pict.addView('RetoldRemote-VLCSetup', libViewVLCSetup.default_configuration, libViewVLCSetup);
61
+ this.pict.addView('RetoldRemote-CollectionsPanel', libViewCollectionsPanel.default_configuration, libViewCollectionsPanel);
56
62
 
57
63
  // Add new providers
58
64
  this.pict.addProvider('RetoldRemote-Provider', libProviderRetoldRemote.default_configuration, libProviderRetoldRemote);
@@ -60,6 +66,10 @@ class RetoldRemoteApplication extends libContentEditorApplication
60
66
  this.pict.addProvider('RetoldRemote-GalleryFilterSort', libProviderGalleryFilterSort.default_configuration, libProviderGalleryFilterSort);
61
67
  this.pict.addProvider('RetoldRemote-Icons', libProviderRetoldRemoteIcons.default_configuration, libProviderRetoldRemoteIcons);
62
68
  this.pict.addProvider('RetoldRemote-Theme', libProviderRetoldRemoteTheme.default_configuration, libProviderRetoldRemoteTheme);
69
+ this.pict.addProvider('RetoldRemote-FormattingUtilities', libProviderFormattingUtilities.default_configuration, libProviderFormattingUtilities);
70
+ this.pict.addProvider('RetoldRemote-ToastNotification', libProviderToastNotification.default_configuration, libProviderToastNotification);
71
+ this.pict.addProvider('RetoldRemote-CollectionManager', libProviderCollectionManager.default_configuration, libProviderCollectionManager);
72
+ this.pict.addProvider('RetoldRemote-AISortManager', libProviderAISortManager.default_configuration, libProviderAISortManager);
63
73
  }
64
74
 
65
75
  onAfterInitializeAsync(fCallback)
@@ -98,6 +108,11 @@ class RetoldRemoteApplication extends libContentEditorApplication
98
108
  AutoplayVideo: false,
99
109
  AutoplayAudio: false,
100
110
 
111
+ // List column visibility
112
+ ListShowExtension: true,
113
+ ListShowSize: true,
114
+ ListShowDate: true,
115
+
101
116
  // Filter state
102
117
  FilterState:
103
118
  {
@@ -120,7 +135,28 @@ class RetoldRemoteApplication extends libContentEditorApplication
120
135
  FilterPanelOpen: false,
121
136
 
122
137
  // Saved filter presets
123
- FilterPresets: [] // [{ Name, FilterState, SortField, SortDirection }]
138
+ FilterPresets: [], // [{ Name, FilterState, SortField, SortDirection }]
139
+
140
+ // Collections state
141
+ Collections: [], // Array of collection summaries
142
+ CollectionsPanelOpen: false,
143
+ CollectionsPanelWidth: 300,
144
+ CollectionsPanelMode: 'list', // 'list' | 'detail' | 'edit'
145
+ ActiveCollectionGUID: null,
146
+ ActiveCollection: null,
147
+ CollectionSearchQuery: '',
148
+ LastUsedCollectionGUID: null,
149
+ BrowsingCollection: false, // true when viewer is navigating collection items
150
+ BrowsingCollectionIndex: -1, // index into ActiveCollection.Items
151
+
152
+ // AI Sort settings
153
+ AISortSettings:
154
+ {
155
+ AIEndpoint: 'http://localhost:11434',
156
+ AIModel: 'llama3.1',
157
+ AIProvider: 'ollama',
158
+ NamingTemplate: '{artist}/{album}/{track} - {title}'
159
+ }
124
160
  };
125
161
 
126
162
  // Load persisted settings
@@ -167,6 +203,23 @@ class RetoldRemoteApplication extends libContentEditorApplication
167
203
  // Render the topbar
168
204
  this.pict.views['ContentEditor-TopBar'].render();
169
205
 
206
+ // Render the collections panel (starts collapsed)
207
+ this.pict.views['RetoldRemote-CollectionsPanel'].render();
208
+
209
+ // Fetch collections list
210
+ let tmpCollManager = this.pict.providers['RetoldRemote-CollectionManager'];
211
+ if (tmpCollManager)
212
+ {
213
+ tmpCollManager.fetchCollections();
214
+ }
215
+
216
+ // Update the collections button icon
217
+ let tmpTopBarView = this.pict.views['ContentEditor-TopBar'];
218
+ if (tmpTopBarView && typeof tmpTopBarView.updateCollectionsIcon === 'function')
219
+ {
220
+ tmpTopBarView.updateCollectionsIcon();
221
+ }
222
+
170
223
  let tmpSelf = this;
171
224
 
172
225
  // Wire up file selection from the file browser sidebar
@@ -473,6 +526,9 @@ class RetoldRemoteApplication extends libContentEditorApplication
473
526
  tmpListDetailView.render();
474
527
  }
475
528
 
529
+ // Inject the add-folder button at the bottom of the sidebar file list
530
+ tmpSelf._injectSidebarAddFolderButton();
531
+
476
532
  // Populate raw file list and run filter pipeline
477
533
  let tmpRemote = tmpSelf.pict.AppData.RetoldRemote;
478
534
  tmpRemote.RawFileList = pFileList || [];
@@ -619,6 +675,18 @@ class RetoldRemoteApplication extends libContentEditorApplication
619
675
  tmpAEX.showExplorer(tmpFilePath);
620
676
  }
621
677
  }
678
+ else if (tmpParts[0] === 'collection' && tmpParts.length >= 2)
679
+ {
680
+ let tmpCollectionGUID = tmpParts[1];
681
+ let tmpCollManager = this.pict.providers['RetoldRemote-CollectionManager'];
682
+ if (tmpCollManager)
683
+ {
684
+ let tmpRemote = this.pict.AppData.RetoldRemote;
685
+ tmpRemote.CollectionsPanelMode = 'detail';
686
+ tmpCollManager.openPanel();
687
+ tmpCollManager.fetchCollection(tmpCollectionGUID);
688
+ }
689
+ }
622
690
  else if (tmpParts[0] === 'edit' && tmpParts.length >= 2)
623
691
  {
624
692
  let tmpRawPath = tmpParts.slice(1).join('/');
@@ -627,6 +695,38 @@ class RetoldRemoteApplication extends libContentEditorApplication
627
695
  }
628
696
  }
629
697
 
698
+ /**
699
+ * Inject a subtle "Add Folder" button at the bottom of the sidebar file list.
700
+ * Replaces the bright white "+" button from the breadcrumb bar.
701
+ */
702
+ _injectSidebarAddFolderButton()
703
+ {
704
+ let tmpDetailRows = document.getElementById('Pict-FileBrowser-DetailRows');
705
+ if (!tmpDetailRows)
706
+ {
707
+ return;
708
+ }
709
+
710
+ // Remove any existing injected button so we don't duplicate
711
+ let tmpExisting = tmpDetailRows.parentElement.querySelector('.retold-remote-sidebar-addfolder');
712
+ if (tmpExisting)
713
+ {
714
+ tmpExisting.parentElement.removeChild(tmpExisting);
715
+ }
716
+
717
+ let tmpBtn = document.createElement('button');
718
+ tmpBtn.className = 'retold-remote-sidebar-addfolder';
719
+ tmpBtn.textContent = '+ New Folder';
720
+ tmpBtn.title = 'Create a new folder here';
721
+ tmpBtn.onclick = function()
722
+ {
723
+ pict.PictApplication.promptNewFolder();
724
+ };
725
+
726
+ // Insert after the detail rows container
727
+ tmpDetailRows.parentElement.appendChild(tmpBtn);
728
+ }
729
+
630
730
  /**
631
731
  * Save RetoldRemote settings to localStorage.
632
732
  */
@@ -651,7 +751,14 @@ class RetoldRemoteApplication extends libContentEditorApplication
651
751
  FilterPresets: tmpRemote.FilterPresets,
652
752
  FilterPanelOpen: tmpRemote.FilterPanelOpen,
653
753
  AutoplayVideo: tmpRemote.AutoplayVideo,
654
- AutoplayAudio: tmpRemote.AutoplayAudio
754
+ AutoplayAudio: tmpRemote.AutoplayAudio,
755
+ ListShowExtension: tmpRemote.ListShowExtension,
756
+ ListShowSize: tmpRemote.ListShowSize,
757
+ ListShowDate: tmpRemote.ListShowDate,
758
+ CollectionsPanelOpen: tmpRemote.CollectionsPanelOpen,
759
+ CollectionsPanelWidth: tmpRemote.CollectionsPanelWidth,
760
+ LastUsedCollectionGUID: tmpRemote.LastUsedCollectionGUID,
761
+ AISortSettings: tmpRemote.AISortSettings
655
762
  };
656
763
  localStorage.setItem('retold-remote-settings', JSON.stringify(tmpSettings));
657
764
  }
@@ -693,6 +800,19 @@ class RetoldRemoteApplication extends libContentEditorApplication
693
800
  if (typeof (tmpSettings.FilterPanelOpen) === 'boolean') tmpRemote.FilterPanelOpen = tmpSettings.FilterPanelOpen;
694
801
  if (typeof (tmpSettings.AutoplayVideo) === 'boolean') tmpRemote.AutoplayVideo = tmpSettings.AutoplayVideo;
695
802
  if (typeof (tmpSettings.AutoplayAudio) === 'boolean') tmpRemote.AutoplayAudio = tmpSettings.AutoplayAudio;
803
+ if (typeof (tmpSettings.ListShowExtension) === 'boolean') tmpRemote.ListShowExtension = tmpSettings.ListShowExtension;
804
+ if (typeof (tmpSettings.ListShowSize) === 'boolean') tmpRemote.ListShowSize = tmpSettings.ListShowSize;
805
+ if (typeof (tmpSettings.ListShowDate) === 'boolean') tmpRemote.ListShowDate = tmpSettings.ListShowDate;
806
+ if (typeof (tmpSettings.CollectionsPanelOpen) === 'boolean') tmpRemote.CollectionsPanelOpen = tmpSettings.CollectionsPanelOpen;
807
+ if (tmpSettings.CollectionsPanelWidth) tmpRemote.CollectionsPanelWidth = tmpSettings.CollectionsPanelWidth;
808
+ if (tmpSettings.LastUsedCollectionGUID) tmpRemote.LastUsedCollectionGUID = tmpSettings.LastUsedCollectionGUID;
809
+ if (tmpSettings.AISortSettings && typeof tmpSettings.AISortSettings === 'object')
810
+ {
811
+ if (tmpSettings.AISortSettings.AIEndpoint) tmpRemote.AISortSettings.AIEndpoint = tmpSettings.AISortSettings.AIEndpoint;
812
+ if (tmpSettings.AISortSettings.AIModel) tmpRemote.AISortSettings.AIModel = tmpSettings.AISortSettings.AIModel;
813
+ if (tmpSettings.AISortSettings.AIProvider) tmpRemote.AISortSettings.AIProvider = tmpSettings.AISortSettings.AIProvider;
814
+ if (tmpSettings.AISortSettings.NamingTemplate) tmpRemote.AISortSettings.NamingTemplate = tmpSettings.AISortSettings.NamingTemplate;
815
+ }
696
816
  }
697
817
  }
698
818
  catch (pError)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared extension category maps for retold-remote.
3
+ *
4
+ * Used by both client-side providers (GalleryFilterSort) and server-side
5
+ * services (MediaService) to classify files by extension.
6
+ */
7
+
8
+ const ImageExtensions = { 'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'webp': true, 'svg': true, 'bmp': true, 'ico': true, 'avif': true, 'tiff': true, 'tif': true, 'heic': true, 'heif': true };
9
+ 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 };
10
+ const AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
11
+ const DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true };
12
+
13
+ /**
14
+ * Get the media category for a file extension.
15
+ *
16
+ * @param {string} pExtension - Extension with or without leading dot (e.g. '.png' or 'png')
17
+ * @returns {string} 'image', 'video', 'audio', 'document', or 'other'
18
+ */
19
+ function getCategory(pExtension)
20
+ {
21
+ let tmpExt = (pExtension || '').replace(/^\./, '').toLowerCase();
22
+ if (ImageExtensions[tmpExt]) return 'image';
23
+ if (VideoExtensions[tmpExt]) return 'video';
24
+ if (AudioExtensions[tmpExt]) return 'audio';
25
+ if (DocumentExtensions[tmpExt]) return 'document';
26
+ return 'other';
27
+ }
28
+
29
+ module.exports.ImageExtensions = ImageExtensions;
30
+ module.exports.VideoExtensions = VideoExtensions;
31
+ module.exports.AudioExtensions = AudioExtensions;
32
+ module.exports.DocumentExtensions = DocumentExtensions;
33
+ module.exports.getCategory = getCategory;
@@ -11,16 +11,16 @@
11
11
  * server setup) so that we control all routes and avoid conflicts with editor-
12
12
  * specific endpoints (save, upload) that aren't needed here.
13
13
  *
14
+ * Cache storage is managed by Parime's BinaryStorage with configurable
15
+ * hash-based subfolder sharding to avoid huge flat directories.
16
+ *
14
17
  * @param {object} pOptions
15
18
  * @param {string} pOptions.ContentPath - Absolute path to the media folder to browse
16
19
  * @param {string} pOptions.DistPath - Absolute path to the built web-application folder
17
20
  * @param {number} pOptions.Port - HTTP port
18
21
  * @param {boolean} [pOptions.HashedFilenames] - Enable hashed filenames mode
19
22
  * @param {string} [pOptions.CacheRoot] - Root cache directory (default: ./dist/retold-cache/)
20
- * @param {string} [pOptions.CacheThumbnails] - Override thumbnails cache directory
21
- * @param {string} [pOptions.CacheArchives] - Override archives cache directory
22
- * @param {string} [pOptions.CacheVideoFrames] - Override video-frames cache directory
23
- * @param {string} [pOptions.CacheAudioWaveforms] - Override audio-waveforms cache directory
23
+ * @param {string} [pOptions.CacheServer] - URL to a remote parime cache server
24
24
  * @param {Function} fCallback - Callback(pError, { Fable, Orator, Port })
25
25
  */
26
26
 
@@ -33,12 +33,19 @@ const libOrator = require('orator');
33
33
  const libOratorServiceServerRestify = require('orator-serviceserver-restify');
34
34
  const libFileBrowserService = require('pict-section-filebrowser').FileBrowserService;
35
35
 
36
+ const libParimeStorage = require('parime/storage');
37
+ const libRetoldRemoteParimeCache = require('../server/RetoldRemote-ParimeCache.js');
38
+
36
39
  const libRetoldRemoteMediaService = require('../server/RetoldRemote-MediaService.js');
37
40
  const libRetoldRemotePathRegistry = require('../server/RetoldRemote-PathRegistry.js');
38
41
  const libRetoldRemoteArchiveService = require('../server/RetoldRemote-ArchiveService.js');
39
42
  const libRetoldRemoteVideoFrameService = require('../server/RetoldRemote-VideoFrameService.js');
40
43
  const libRetoldRemoteAudioWaveformService = require('../server/RetoldRemote-AudioWaveformService.js');
41
44
  const libRetoldRemoteEbookService = require('../server/RetoldRemote-EbookService.js');
45
+ const libRetoldRemoteCollectionService = require('../server/RetoldRemote-CollectionService.js');
46
+ const libRetoldRemoteMetadataCache = require('../server/RetoldRemote-MetadataCache.js');
47
+ const libRetoldRemoteFileOperationService = require('../server/RetoldRemote-FileOperationService.js');
48
+ const libRetoldRemoteAISortService = require('../server/RetoldRemote-AISortService.js');
42
49
  const libUrl = require('url');
43
50
 
44
51
  function setupRetoldRemoteServer(pOptions, fCallback)
@@ -46,16 +53,33 @@ function setupRetoldRemoteServer(pOptions, fCallback)
46
53
  let tmpContentPath = pOptions.ContentPath;
47
54
  let tmpDistFolder = pOptions.DistPath;
48
55
  let tmpPort = pOptions.Port;
49
- let tmpHashedFilenames = !!(pOptions.HashedFilenames || process.env.RETOLD_HASHED_FILENAMES === 'true');
56
+ let tmpHashedFilenames = (pOptions.HashedFilenames !== false) && (process.env.RETOLD_HASHED_FILENAMES !== 'false');
57
+
58
+ // --- Resolve cache root ---
59
+ let tmpCacheRoot = pOptions.CacheRoot
60
+ || libPath.resolve(process.cwd(), 'dist', 'retold-cache');
50
61
 
51
62
  let tmpSettings =
52
63
  {
53
64
  Product: 'Retold-Remote',
54
65
  ProductVersion: require('../../package.json').version,
55
66
  APIServerPort: tmpPort,
56
- ContentPath: tmpContentPath
67
+ ContentPath: tmpContentPath,
68
+ ParimeBinaryStorageRoot: tmpCacheRoot,
69
+ ParimeBinarySharding:
70
+ {
71
+ Enabled: true,
72
+ SegmentSize: 2,
73
+ Depth: 4
74
+ }
57
75
  };
58
76
 
77
+ // If a remote cache server is specified, route cache operations over HTTP
78
+ if (pOptions.CacheServer)
79
+ {
80
+ tmpSettings.ParimeCacheServer = pOptions.CacheServer;
81
+ }
82
+
59
83
  let tmpFable = new libFable(tmpSettings);
60
84
 
61
85
  // Ensure the content directory exists
@@ -89,68 +113,79 @@ function setupRetoldRemoteServer(pOptions, fCallback)
89
113
  tmpFable.log.info('Hashed filenames mode: ENABLED');
90
114
  }
91
115
 
92
- // --- Resolve cache paths ---
93
- // Default root: ./dist/retold-cache/ relative to process cwd.
94
- // Individual overrides take precedence over root.
95
- let tmpCacheRoot = pOptions.CacheRoot
96
- || libPath.resolve(process.cwd(), 'dist', 'retold-cache');
116
+ // --- Initialize Parime storage ---
117
+ tmpFable.serviceManager.addServiceType('ParimeStorage', libParimeStorage);
118
+ let tmpParimeStorage = tmpFable.serviceManager.instantiateServiceProvider('ParimeStorage');
97
119
 
98
- let tmpCacheThumbnails = pOptions.CacheThumbnails
99
- || libPath.join(tmpCacheRoot, 'thumbnails');
100
- let tmpCacheArchives = pOptions.CacheArchives
101
- || libPath.join(tmpCacheRoot, 'archives');
102
- let tmpCacheVideoFrames = pOptions.CacheVideoFrames
103
- || libPath.join(tmpCacheRoot, 'video-frames');
104
- let tmpCacheAudioWaveforms = pOptions.CacheAudioWaveforms
105
- || libPath.join(tmpCacheRoot, 'audio-waveforms');
106
- let tmpCacheEbooks = pOptions.CacheEbooks
107
- || libPath.join(tmpCacheRoot, 'ebook-conversions');
108
-
109
- tmpFable.log.info(`Cache root: ${tmpCacheRoot}`);
110
- tmpFable.log.info(` Thumbnails: ${tmpCacheThumbnails}`);
111
- tmpFable.log.info(` Archives: ${tmpCacheArchives}`);
112
- tmpFable.log.info(` Video frames: ${tmpCacheVideoFrames}`);
113
- tmpFable.log.info(` Audio waveforms: ${tmpCacheAudioWaveforms}`);
114
- tmpFable.log.info(` Ebook conversions: ${tmpCacheEbooks}`);
115
-
116
- // Set up the archive service
117
- let tmpArchiveService = new libRetoldRemoteArchiveService(tmpFable,
118
- {
119
- ContentPath: tmpContentPath,
120
- CachePath: tmpCacheArchives
121
- });
120
+ tmpFable.serviceManager.addServiceType('RetoldRemoteParimeCache', libRetoldRemoteParimeCache);
121
+ let tmpParimeCache = tmpFable.serviceManager.instantiateServiceProvider('RetoldRemoteParimeCache');
122
122
 
123
- // Set up the video frame service
124
- let tmpVideoFrameService = new libRetoldRemoteVideoFrameService(tmpFable,
125
- {
126
- ContentPath: tmpContentPath,
127
- CachePath: tmpCacheVideoFrames
128
- });
123
+ tmpParimeStorage.initialize(
124
+ (pStorageError) =>
125
+ {
126
+ if (pStorageError)
127
+ {
128
+ return fCallback(pStorageError);
129
+ }
129
130
 
130
- // Set up the audio waveform service
131
- let tmpAudioWaveformService = new libRetoldRemoteAudioWaveformService(tmpFable,
132
- {
133
- ContentPath: tmpContentPath,
134
- CachePath: tmpCacheAudioWaveforms
135
- });
131
+ tmpFable.log.info(`Cache storage: ${tmpParimeCache.isRemote ? 'REMOTE (' + pOptions.CacheServer + ')' : tmpCacheRoot}`);
136
132
 
137
- // Set up the ebook conversion service
138
- let tmpEbookService = new libRetoldRemoteEbookService(tmpFable,
139
- {
140
- ContentPath: tmpContentPath,
141
- CachePath: tmpCacheEbooks
142
- });
133
+ // Set up the archive service
134
+ let tmpArchiveService = new libRetoldRemoteArchiveService(tmpFable,
135
+ {
136
+ ContentPath: tmpContentPath
137
+ });
143
138
 
144
- // Set up the media service
145
- let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
146
- {
147
- ContentPath: tmpContentPath,
148
- ThumbnailCachePath: tmpCacheThumbnails,
149
- APIRoutePrefix: '/api/media',
150
- PathRegistry: tmpPathRegistry
151
- });
139
+ // Set up the video frame service
140
+ let tmpVideoFrameService = new libRetoldRemoteVideoFrameService(tmpFable,
141
+ {
142
+ ContentPath: tmpContentPath
143
+ });
144
+
145
+ // Set up the audio waveform service
146
+ let tmpAudioWaveformService = new libRetoldRemoteAudioWaveformService(tmpFable,
147
+ {
148
+ ContentPath: tmpContentPath
149
+ });
150
+
151
+ // Set up the ebook conversion service
152
+ let tmpEbookService = new libRetoldRemoteEbookService(tmpFable,
153
+ {
154
+ ContentPath: tmpContentPath
155
+ });
156
+
157
+ // Set up the metadata cache service
158
+ let tmpMetadataCache = new libRetoldRemoteMetadataCache(tmpFable,
159
+ {
160
+ ContentPath: tmpContentPath
161
+ });
162
+
163
+ // Set up the file operation service
164
+ let tmpFileOperationService = new libRetoldRemoteFileOperationService(tmpFable,
165
+ {
166
+ ContentPath: tmpContentPath
167
+ });
168
+
169
+ // Set up the AI sort service
170
+ let tmpAISortService = new libRetoldRemoteAISortService(tmpFable,
171
+ {
172
+ ContentPath: tmpContentPath
173
+ });
174
+ tmpAISortService.setMetadataCache(tmpMetadataCache);
175
+
176
+ // Set up the collection service
177
+ let tmpCollectionService = new libRetoldRemoteCollectionService(tmpFable, {});
178
+ tmpCollectionService.setFileOperationService(tmpFileOperationService);
152
179
 
153
- tmpOrator.initialize(
180
+ // Set up the media service
181
+ let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
182
+ {
183
+ ContentPath: tmpContentPath,
184
+ APIRoutePrefix: '/api/media',
185
+ PathRegistry: tmpPathRegistry
186
+ });
187
+
188
+ tmpOrator.initialize(
154
189
  function ()
155
190
  {
156
191
  let tmpServiceServer = tmpOrator.serviceServer;
@@ -196,13 +231,11 @@ function setupRetoldRemoteServer(pOptions, fCallback)
196
231
  {
197
232
  Success: true,
198
233
  HashedFilenames: tmpHashedFilenames,
199
- CachePaths:
234
+ CacheStorage:
200
235
  {
201
236
  Root: tmpCacheRoot,
202
- Thumbnails: tmpCacheThumbnails,
203
- Archives: tmpCacheArchives,
204
- VideoFrames: tmpCacheVideoFrames,
205
- AudioWaveforms: tmpCacheAudioWaveforms
237
+ Remote: tmpParimeCache.isRemote,
238
+ Sharding: true
206
239
  },
207
240
  ArchiveSupport:
208
241
  {
@@ -323,6 +356,113 @@ function setupRetoldRemoteServer(pOptions, fCallback)
323
356
  // Connect media service API routes
324
357
  tmpMediaService.connectRoutes(tmpServiceServer);
325
358
 
359
+ // Connect file operation service API routes
360
+ tmpFileOperationService.connectRoutes(tmpServiceServer);
361
+
362
+ // Connect collection service API routes
363
+ tmpCollectionService.connectRoutes(tmpServiceServer);
364
+
365
+ // Connect AI sort service API routes
366
+ tmpAISortService.connectRoutes(tmpServiceServer);
367
+
368
+ // --- GET /api/media/metadata ---
369
+ // Get cached metadata (with ID3/format tags) for a single file.
370
+ tmpServiceServer.get('/api/media/metadata',
371
+ (pRequest, pResponse, fNext) =>
372
+ {
373
+ try
374
+ {
375
+ let tmpParsedUrl = libUrl.parse(pRequest.url, true);
376
+ let tmpQuery = tmpParsedUrl.query;
377
+ let tmpRelPath = tmpQuery.path;
378
+
379
+ if (!tmpRelPath || typeof (tmpRelPath) !== 'string')
380
+ {
381
+ pResponse.send(400, { Success: false, Error: 'Missing path parameter.' });
382
+ return fNext();
383
+ }
384
+
385
+ tmpRelPath = decodeURIComponent(tmpRelPath).replace(/^\/+/, '');
386
+ if (tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
387
+ {
388
+ pResponse.send(400, { Success: false, Error: 'Invalid path.' });
389
+ return fNext();
390
+ }
391
+
392
+ tmpMetadataCache.getMetadata(tmpRelPath,
393
+ (pError, pMetadata) =>
394
+ {
395
+ if (pError)
396
+ {
397
+ pResponse.send(404, { Success: false, Error: pError.message });
398
+ return fNext();
399
+ }
400
+ pResponse.send(pMetadata);
401
+ return fNext();
402
+ });
403
+ }
404
+ catch (pError)
405
+ {
406
+ pResponse.send(500, { Success: false, Error: pError.message });
407
+ return fNext();
408
+ }
409
+ });
410
+
411
+ // --- POST /api/media/metadata-batch ---
412
+ // Get cached metadata for multiple files at once.
413
+ tmpServiceServer.post('/api/media/metadata-batch',
414
+ (pRequest, pResponse, fNext) =>
415
+ {
416
+ try
417
+ {
418
+ let tmpPaths = pRequest.body && pRequest.body.Paths;
419
+
420
+ if (!Array.isArray(tmpPaths) || tmpPaths.length === 0)
421
+ {
422
+ pResponse.send(400, { Success: false, Error: 'Missing Paths array.' });
423
+ return fNext();
424
+ }
425
+
426
+ // Sanitize all paths
427
+ let tmpSanitized = [];
428
+ for (let i = 0; i < tmpPaths.length; i++)
429
+ {
430
+ let tmpRelPath = (typeof (tmpPaths[i]) === 'string')
431
+ ? decodeURIComponent(tmpPaths[i]).replace(/^\/+/, '')
432
+ : null;
433
+
434
+ if (!tmpRelPath || tmpRelPath.includes('..') || libPath.isAbsolute(tmpRelPath))
435
+ {
436
+ continue;
437
+ }
438
+ tmpSanitized.push(tmpRelPath);
439
+ }
440
+
441
+ if (tmpSanitized.length === 0)
442
+ {
443
+ pResponse.send(400, { Success: false, Error: 'No valid paths.' });
444
+ return fNext();
445
+ }
446
+
447
+ tmpMetadataCache.getMetadataBatch(tmpSanitized,
448
+ (pError, pResults) =>
449
+ {
450
+ if (pError)
451
+ {
452
+ pResponse.send(500, { Success: false, Error: pError.message });
453
+ return fNext();
454
+ }
455
+ pResponse.send({ Success: true, Files: pResults });
456
+ return fNext();
457
+ });
458
+ }
459
+ catch (pError)
460
+ {
461
+ pResponse.send(500, { Success: false, Error: pError.message });
462
+ return fNext();
463
+ }
464
+ });
465
+
326
466
  // --- GET /api/media/video-frames ---
327
467
  // Extract evenly-spaced frames from a video for the Video Explorer.
328
468
  tmpServiceServer.get('/api/media/video-frames',
@@ -1030,10 +1170,14 @@ function setupRetoldRemoteServer(pOptions, fCallback)
1030
1170
  VideoFrameService: tmpVideoFrameService,
1031
1171
  AudioWaveformService: tmpAudioWaveformService,
1032
1172
  PathRegistry: tmpPathRegistry,
1173
+ ParimeCache: tmpParimeCache,
1174
+ MetadataCache: tmpMetadataCache,
1175
+ FileOperationService: tmpFileOperationService,
1033
1176
  Port: tmpPort
1034
1177
  });
1035
1178
  });
1036
1179
  });
1180
+ });
1037
1181
  }
1038
1182
 
1039
1183
  module.exports = setupRetoldRemoteServer;
@@ -19,18 +19,12 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
19
19
  { Name: '-p, --port [port]', Description: 'Port to serve on (defaults to random 7000-7999).', Default: '0' });
20
20
 
21
21
  this.options.CommandOptions.push(
22
- { Name: '-H, --hashed-filenames', Description: 'Enable hashed filenames mode (short hashes instead of full paths in URLs).', Default: false });
22
+ { Name: '--no-hash', Description: 'Disable hashed filenames mode (use plain paths in URLs instead of short hashes).', Default: false });
23
23
 
24
24
  this.options.CommandOptions.push(
25
25
  { Name: '-c, --cache-path [path]', Description: 'Root cache directory (defaults to ./dist/retold-cache/).', Default: '' });
26
26
  this.options.CommandOptions.push(
27
- { Name: '--cache-thumbnails [path]', Description: 'Override thumbnails cache directory.', Default: '' });
28
- this.options.CommandOptions.push(
29
- { Name: '--cache-archives [path]', Description: 'Override archives cache directory.', Default: '' });
30
- this.options.CommandOptions.push(
31
- { Name: '--cache-video-frames [path]', Description: 'Override video-frames cache directory.', Default: '' });
32
- this.options.CommandOptions.push(
33
- { Name: '--cache-audio-waveforms [path]', Description: 'Override audio-waveforms cache directory.', Default: '' });
27
+ { Name: '--cache-server [url]', Description: 'URL of a remote parime cache server (e.g. http://host:9999).', Default: '' });
34
28
 
35
29
  this.addCommand();
36
30
  }
@@ -60,24 +54,12 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
60
54
  let tmpSelf = this;
61
55
  let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
62
56
 
63
- let tmpHashedFilenames = !!(this.CommandOptions.hashedFilenames);
57
+ let tmpHashedFilenames = !(this.CommandOptions.noHash);
64
58
 
65
- // Resolve cache paths: individual overrides > root override > default
66
59
  let tmpCacheRoot = this.CommandOptions.cachePath
67
60
  ? libPath.resolve(this.CommandOptions.cachePath)
68
61
  : null;
69
- let tmpCacheThumbnails = this.CommandOptions.cacheThumbnails
70
- ? libPath.resolve(this.CommandOptions.cacheThumbnails)
71
- : null;
72
- let tmpCacheArchives = this.CommandOptions.cacheArchives
73
- ? libPath.resolve(this.CommandOptions.cacheArchives)
74
- : null;
75
- let tmpCacheVideoFrames = this.CommandOptions.cacheVideoFrames
76
- ? libPath.resolve(this.CommandOptions.cacheVideoFrames)
77
- : null;
78
- let tmpCacheAudioWaveforms = this.CommandOptions.cacheAudioWaveforms
79
- ? libPath.resolve(this.CommandOptions.cacheAudioWaveforms)
80
- : null;
62
+ let tmpCacheServer = this.CommandOptions.cacheServer || null;
81
63
 
82
64
  tmpSetupServer(
83
65
  {
@@ -86,10 +68,7 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
86
68
  Port: tmpPort,
87
69
  HashedFilenames: tmpHashedFilenames,
88
70
  CacheRoot: tmpCacheRoot,
89
- CacheThumbnails: tmpCacheThumbnails,
90
- CacheArchives: tmpCacheArchives,
91
- CacheVideoFrames: tmpCacheVideoFrames,
92
- CacheAudioWaveforms: tmpCacheAudioWaveforms
71
+ CacheServer: tmpCacheServer
93
72
  },
94
73
  function (pError, pServerInfo)
95
74
  {