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.
- package/css/retold-remote.css +3 -0
- package/docs/README.md +181 -0
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +10 -0
- package/docs/_topbar.md +3 -0
- package/docs/audio-viewer.md +133 -0
- package/docs/ebook-reader.md +90 -0
- package/docs/image-viewer.md +90 -0
- package/docs/server-setup.md +262 -0
- package/docs/video-viewer.md +134 -0
- package/html/docs.html +59 -0
- package/html/index.html +1 -1
- package/package.json +20 -6
- package/source/Pict-Application-RetoldRemote.js +122 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +211 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
- package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
- package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
- package/source/providers/Pict-Provider-ToastNotification.js +96 -0
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
- package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
- package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +843 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +15 -14
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/server/RetoldRemote-ParimeCache.js +349 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
- package/source/server/RetoldRemote-VideoFrameService.js +7 -15
- package/source/views/PictView-Remote-AudioExplorer.js +10 -43
- package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
- package/source/views/PictView-Remote-Gallery.js +237 -44
- package/source/views/PictView-Remote-ImageViewer.js +1 -34
- package/source/views/PictView-Remote-Layout.js +411 -20
- package/source/views/PictView-Remote-MediaViewer.js +311 -28
- package/source/views/PictView-Remote-SettingsPanel.js +275 -133
- package/source/views/PictView-Remote-TopBar.js +701 -14
- package/source/views/PictView-Remote-VLCSetup.js +8 -39
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/docs/README.md +181 -0
- package/web-application/docs/_cover.md +14 -0
- package/web-application/docs/_sidebar.md +10 -0
- package/web-application/docs/_topbar.md +3 -0
- package/web-application/docs/audio-viewer.md +133 -0
- package/web-application/docs/ebook-reader.md +90 -0
- package/web-application/docs/image-viewer.md +90 -0
- package/web-application/docs/server-setup.md +262 -0
- package/web-application/docs/video-viewer.md +134 -0
- package/web-application/docs.html +59 -0
- package/web-application/index.html +1 -1
- package/web-application/js/pict-docuserve.min.js +58 -0
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +2652 -970
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +60 -13
- package/web-application/retold-remote.min.js.map +1 -1
- 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: []
|
|
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.
|
|
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 =
|
|
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
|
-
// ---
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
tmpParimeStorage.initialize(
|
|
124
|
+
(pStorageError) =>
|
|
125
|
+
{
|
|
126
|
+
if (pStorageError)
|
|
127
|
+
{
|
|
128
|
+
return fCallback(pStorageError);
|
|
129
|
+
}
|
|
129
130
|
|
|
130
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
133
|
+
// Set up the archive service
|
|
134
|
+
let tmpArchiveService = new libRetoldRemoteArchiveService(tmpFable,
|
|
135
|
+
{
|
|
136
|
+
ContentPath: tmpContentPath
|
|
137
|
+
});
|
|
143
138
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
CacheStorage:
|
|
200
235
|
{
|
|
201
236
|
Root: tmpCacheRoot,
|
|
202
|
-
|
|
203
|
-
|
|
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: '
|
|
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-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
90
|
-
CacheArchives: tmpCacheArchives,
|
|
91
|
-
CacheVideoFrames: tmpCacheVideoFrames,
|
|
92
|
-
CacheAudioWaveforms: tmpCacheAudioWaveforms
|
|
71
|
+
CacheServer: tmpCacheServer
|
|
93
72
|
},
|
|
94
73
|
function (pError, pServerInfo)
|
|
95
74
|
{
|