retold-remote 0.0.6 → 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/html/index.html +1 -1
- package/package.json +1 -1
- package/source/Pict-Application-RetoldRemote.js +21 -2
- package/source/cli/RetoldRemote-Server-Setup.js +129 -0
- package/source/providers/Pict-Provider-AISortManager.js +456 -0
- package/source/providers/Pict-Provider-CollectionManager.js +266 -0
- package/source/server/RetoldRemote-AISortService.js +879 -0
- package/source/server/RetoldRemote-CollectionService.js +161 -2
- package/source/server/RetoldRemote-FileOperationService.js +560 -0
- package/source/server/RetoldRemote-MediaService.js +12 -0
- package/source/server/RetoldRemote-MetadataCache.js +411 -0
- package/source/views/PictView-Remote-CollectionsPanel.js +435 -36
- package/source/views/PictView-Remote-Layout.js +2 -0
- package/source/views/PictView-Remote-SettingsPanel.js +156 -0
- package/source/views/PictView-Remote-TopBar.js +86 -0
- package/web-application/css/retold-remote.css +3 -0
- package/web-application/index.html +1 -1
- package/web-application/retold-remote.js +402 -34
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +32 -15
- package/web-application/retold-remote.min.js.map +1 -1
package/css/retold-remote.css
CHANGED
|
@@ -12,6 +12,7 @@ html, body
|
|
|
12
12
|
margin: 0;
|
|
13
13
|
padding: 0;
|
|
14
14
|
height: 100%;
|
|
15
|
+
height: 100dvh;
|
|
15
16
|
overflow: hidden;
|
|
16
17
|
background: var(--retold-bg-primary);
|
|
17
18
|
color: var(--retold-text-primary);
|
|
@@ -20,6 +21,8 @@ html, body
|
|
|
20
21
|
line-height: 1.5;
|
|
21
22
|
-webkit-font-smoothing: antialiased;
|
|
22
23
|
-moz-osx-font-smoothing: grayscale;
|
|
24
|
+
/* Safe area insets for notched devices */
|
|
25
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/* Scrollbar styling */
|
package/html/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
6
|
<title>Retold Remote</title>
|
|
7
7
|
<link rel="stylesheet" href="css/retold-remote.css">
|
|
8
8
|
<style id="PICT-CSS"></style>
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ const libProviderRetoldRemoteTheme = require('./providers/Pict-Provider-RetoldRe
|
|
|
11
11
|
const libProviderFormattingUtilities = require('./providers/Pict-Provider-FormattingUtilities.js');
|
|
12
12
|
const libProviderToastNotification = require('./providers/Pict-Provider-ToastNotification.js');
|
|
13
13
|
const libProviderCollectionManager = require('./providers/Pict-Provider-CollectionManager.js');
|
|
14
|
+
const libProviderAISortManager = require('./providers/Pict-Provider-AISortManager.js');
|
|
14
15
|
|
|
15
16
|
// Views (replace parent views)
|
|
16
17
|
const libViewLayout = require('./views/PictView-Remote-Layout.js');
|
|
@@ -68,6 +69,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
|
|
|
68
69
|
this.pict.addProvider('RetoldRemote-FormattingUtilities', libProviderFormattingUtilities.default_configuration, libProviderFormattingUtilities);
|
|
69
70
|
this.pict.addProvider('RetoldRemote-ToastNotification', libProviderToastNotification.default_configuration, libProviderToastNotification);
|
|
70
71
|
this.pict.addProvider('RetoldRemote-CollectionManager', libProviderCollectionManager.default_configuration, libProviderCollectionManager);
|
|
72
|
+
this.pict.addProvider('RetoldRemote-AISortManager', libProviderAISortManager.default_configuration, libProviderAISortManager);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
onAfterInitializeAsync(fCallback)
|
|
@@ -145,7 +147,16 @@ class RetoldRemoteApplication extends libContentEditorApplication
|
|
|
145
147
|
CollectionSearchQuery: '',
|
|
146
148
|
LastUsedCollectionGUID: null,
|
|
147
149
|
BrowsingCollection: false, // true when viewer is navigating collection items
|
|
148
|
-
BrowsingCollectionIndex: -1
|
|
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
|
+
}
|
|
149
160
|
};
|
|
150
161
|
|
|
151
162
|
// Load persisted settings
|
|
@@ -746,7 +757,8 @@ class RetoldRemoteApplication extends libContentEditorApplication
|
|
|
746
757
|
ListShowDate: tmpRemote.ListShowDate,
|
|
747
758
|
CollectionsPanelOpen: tmpRemote.CollectionsPanelOpen,
|
|
748
759
|
CollectionsPanelWidth: tmpRemote.CollectionsPanelWidth,
|
|
749
|
-
LastUsedCollectionGUID: tmpRemote.LastUsedCollectionGUID
|
|
760
|
+
LastUsedCollectionGUID: tmpRemote.LastUsedCollectionGUID,
|
|
761
|
+
AISortSettings: tmpRemote.AISortSettings
|
|
750
762
|
};
|
|
751
763
|
localStorage.setItem('retold-remote-settings', JSON.stringify(tmpSettings));
|
|
752
764
|
}
|
|
@@ -794,6 +806,13 @@ class RetoldRemoteApplication extends libContentEditorApplication
|
|
|
794
806
|
if (typeof (tmpSettings.CollectionsPanelOpen) === 'boolean') tmpRemote.CollectionsPanelOpen = tmpSettings.CollectionsPanelOpen;
|
|
795
807
|
if (tmpSettings.CollectionsPanelWidth) tmpRemote.CollectionsPanelWidth = tmpSettings.CollectionsPanelWidth;
|
|
796
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
|
+
}
|
|
797
816
|
}
|
|
798
817
|
}
|
|
799
818
|
catch (pError)
|
|
@@ -43,6 +43,9 @@ const libRetoldRemoteVideoFrameService = require('../server/RetoldRemote-VideoFr
|
|
|
43
43
|
const libRetoldRemoteAudioWaveformService = require('../server/RetoldRemote-AudioWaveformService.js');
|
|
44
44
|
const libRetoldRemoteEbookService = require('../server/RetoldRemote-EbookService.js');
|
|
45
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');
|
|
46
49
|
const libUrl = require('url');
|
|
47
50
|
|
|
48
51
|
function setupRetoldRemoteServer(pOptions, fCallback)
|
|
@@ -151,8 +154,28 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
151
154
|
ContentPath: tmpContentPath
|
|
152
155
|
});
|
|
153
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
|
+
|
|
154
176
|
// Set up the collection service
|
|
155
177
|
let tmpCollectionService = new libRetoldRemoteCollectionService(tmpFable, {});
|
|
178
|
+
tmpCollectionService.setFileOperationService(tmpFileOperationService);
|
|
156
179
|
|
|
157
180
|
// Set up the media service
|
|
158
181
|
let tmpMediaService = new libRetoldRemoteMediaService(tmpFable,
|
|
@@ -333,9 +356,113 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
333
356
|
// Connect media service API routes
|
|
334
357
|
tmpMediaService.connectRoutes(tmpServiceServer);
|
|
335
358
|
|
|
359
|
+
// Connect file operation service API routes
|
|
360
|
+
tmpFileOperationService.connectRoutes(tmpServiceServer);
|
|
361
|
+
|
|
336
362
|
// Connect collection service API routes
|
|
337
363
|
tmpCollectionService.connectRoutes(tmpServiceServer);
|
|
338
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
|
+
|
|
339
466
|
// --- GET /api/media/video-frames ---
|
|
340
467
|
// Extract evenly-spaced frames from a video for the Video Explorer.
|
|
341
468
|
tmpServiceServer.get('/api/media/video-frames',
|
|
@@ -1044,6 +1171,8 @@ function setupRetoldRemoteServer(pOptions, fCallback)
|
|
|
1044
1171
|
AudioWaveformService: tmpAudioWaveformService,
|
|
1045
1172
|
PathRegistry: tmpPathRegistry,
|
|
1046
1173
|
ParimeCache: tmpParimeCache,
|
|
1174
|
+
MetadataCache: tmpMetadataCache,
|
|
1175
|
+
FileOperationService: tmpFileOperationService,
|
|
1047
1176
|
Port: tmpPort
|
|
1048
1177
|
});
|
|
1049
1178
|
});
|
|
@@ -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;
|