incyclist-services 1.7.52 → 1.7.54
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/lib/cjs/devices/ride/service.js +2 -0
- package/lib/cjs/routes/library/service.js +328 -114
- package/lib/cjs/routes/list/loaders/db.js +3 -1
- package/lib/cjs/routes/list/service.js +28 -0
- package/lib/cjs/routes/page/service.js +66 -65
- package/lib/esm/devices/ride/service.js +2 -0
- package/lib/esm/routes/library/service.js +328 -114
- package/lib/esm/routes/list/loaders/db.js +1 -0
- package/lib/esm/routes/list/service.js +28 -0
- package/lib/esm/routes/page/service.js +66 -65
- package/lib/types/routes/base/types/index.d.ts +1 -0
- package/lib/types/routes/library/service.d.ts +26 -11
- package/lib/types/routes/library/types.d.ts +32 -19
- package/lib/types/routes/list/loaders/db.d.ts +1 -0
- package/lib/types/routes/list/service.d.ts +6 -1
- package/lib/types/routes/page/service.d.ts +9 -7
- package/lib/types/routes/page/types.d.ts +2 -5
- package/package.json +1 -1
|
@@ -572,6 +572,8 @@ let DeviceRideService = (() => {
|
|
|
572
572
|
if (startType === 'start') {
|
|
573
573
|
this.initForStart(ai, startProps, route, startPos, realityFactor, rideMode);
|
|
574
574
|
}
|
|
575
|
+
ai.adapter.resumeLogging();
|
|
576
|
+
this.resumeLogging();
|
|
575
577
|
const sType = (ai.isControl) ? 'bike' : 'sensor';
|
|
576
578
|
const logProps = {};
|
|
577
579
|
logProps[sType] = ai.adapter.getUniqueName();
|
|
@@ -44,8 +44,10 @@ const types_1 = require("../../base/types");
|
|
|
44
44
|
const parsers_1 = require("../base/parsers");
|
|
45
45
|
const service_2 = require("../list/service");
|
|
46
46
|
const utils_1 = require("../../utils");
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
const db_1 = require("../list/loaders/db");
|
|
48
|
+
const route_1 = require("../base/model/route");
|
|
49
|
+
const sleep_1 = require("../../utils/sleep");
|
|
50
|
+
const i18n_1 = require("../../i18n");
|
|
49
51
|
let RouteLibraryScannerService = (() => {
|
|
50
52
|
let _classDecorators = [decorators_1.Singleton];
|
|
51
53
|
let _classDescriptor;
|
|
@@ -55,53 +57,220 @@ let RouteLibraryScannerService = (() => {
|
|
|
55
57
|
let _instanceExtraInitializers = [];
|
|
56
58
|
let _getRouteList_decorators;
|
|
57
59
|
let _getBindings_decorators;
|
|
60
|
+
let _getRoutesDBLoader_decorators;
|
|
61
|
+
let _getParsers_decorators;
|
|
62
|
+
let _getUnitConverter_decorators;
|
|
58
63
|
var RouteLibraryScannerService = class extends _classSuper {
|
|
59
64
|
static { _classThis = this; }
|
|
60
65
|
static {
|
|
61
66
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
62
67
|
_getRouteList_decorators = [decorators_1.Injectable];
|
|
63
68
|
_getBindings_decorators = [decorators_1.Injectable];
|
|
69
|
+
_getRoutesDBLoader_decorators = [decorators_1.Injectable];
|
|
70
|
+
_getParsers_decorators = [decorators_1.Injectable];
|
|
71
|
+
_getUnitConverter_decorators = [decorators_1.Injectable];
|
|
64
72
|
__esDecorate(this, null, _getRouteList_decorators, { kind: "method", name: "getRouteList", static: false, private: false, access: { has: obj => "getRouteList" in obj, get: obj => obj.getRouteList }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
65
73
|
__esDecorate(this, null, _getBindings_decorators, { kind: "method", name: "getBindings", static: false, private: false, access: { has: obj => "getBindings" in obj, get: obj => obj.getBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
74
|
+
__esDecorate(this, null, _getRoutesDBLoader_decorators, { kind: "method", name: "getRoutesDBLoader", static: false, private: false, access: { has: obj => "getRoutesDBLoader" in obj, get: obj => obj.getRoutesDBLoader }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
75
|
+
__esDecorate(this, null, _getParsers_decorators, { kind: "method", name: "getParsers", static: false, private: false, access: { has: obj => "getParsers" in obj, get: obj => obj.getParsers }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
76
|
+
__esDecorate(this, null, _getUnitConverter_decorators, { kind: "method", name: "getUnitConverter", static: false, private: false, access: { has: obj => "getUnitConverter" in obj, get: obj => obj.getUnitConverter }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
66
77
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
67
78
|
RouteLibraryScannerService = _classThis = _classDescriptor.value;
|
|
68
79
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
69
80
|
__runInitializers(_classThis, _classExtraInitializers);
|
|
70
81
|
}
|
|
82
|
+
isCancelled = (__runInitializers(this, _instanceExtraInitializers), false);
|
|
83
|
+
scanResult = [];
|
|
84
|
+
importProps;
|
|
71
85
|
constructor() {
|
|
72
86
|
super('RouteLibraryScanner');
|
|
73
|
-
|
|
87
|
+
}
|
|
88
|
+
prepare() {
|
|
89
|
+
this.importProps = {
|
|
90
|
+
phase: 'landing',
|
|
91
|
+
routes: []
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
done() {
|
|
95
|
+
this.importProps = undefined;
|
|
96
|
+
}
|
|
97
|
+
getDisplayProps() {
|
|
98
|
+
return this.importProps;
|
|
99
|
+
}
|
|
100
|
+
importSingle(fileInfo) {
|
|
101
|
+
const observer = new types_1.Observer();
|
|
102
|
+
this.isCancelled = false;
|
|
103
|
+
this.importRoute(fileInfo, observer).catch(err => {
|
|
104
|
+
this.logError(err, 'importSingle', { file: fileInfo?.filename });
|
|
105
|
+
observer.emit('error', err.message);
|
|
106
|
+
});
|
|
107
|
+
observer.on('success', (route) => {
|
|
108
|
+
this.importProps.phase = 'result';
|
|
109
|
+
this.importProps.resultSuccess = { routeName: route.title };
|
|
110
|
+
});
|
|
111
|
+
observer.on('error', (error) => {
|
|
112
|
+
this.importProps.phase = 'result';
|
|
113
|
+
this.importProps.error = error;
|
|
114
|
+
});
|
|
115
|
+
return observer;
|
|
74
116
|
}
|
|
75
117
|
scan(folderInfo) {
|
|
118
|
+
if (!this.importProps)
|
|
119
|
+
this.prepare();
|
|
120
|
+
this.isCancelled = false;
|
|
121
|
+
this.scanResult = [];
|
|
122
|
+
this.importProps.phase = 'scanning';
|
|
76
123
|
const observer = new types_1.Observer();
|
|
77
124
|
this._scan(folderInfo, observer).catch(err => {
|
|
78
125
|
this.logError(err, 'scan', { uri: folderInfo.uri });
|
|
79
126
|
observer.emit('error', err.message);
|
|
80
127
|
});
|
|
128
|
+
observer.on('scan-progress', (progress) => {
|
|
129
|
+
this.importProps.scanProgress = progress;
|
|
130
|
+
});
|
|
81
131
|
return observer;
|
|
82
132
|
}
|
|
83
|
-
|
|
133
|
+
parse(scannedRoutes) {
|
|
84
134
|
const observer = new types_1.Observer();
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
135
|
+
if (!this.importProps)
|
|
136
|
+
this.prepare();
|
|
137
|
+
this._parse(scannedRoutes, observer).catch(err => {
|
|
138
|
+
this.logError(err, 'parse');
|
|
87
139
|
observer.emit('error', err.message);
|
|
88
140
|
});
|
|
141
|
+
observer.on('parse-progress', (progress) => {
|
|
142
|
+
const { parsed, total } = progress;
|
|
143
|
+
this.importProps.parseProgress = { parsed, total };
|
|
144
|
+
});
|
|
145
|
+
observer.on('parse-result', (route) => {
|
|
146
|
+
this.importProps.routes.push(this.buildRouteDisplayItem(route));
|
|
147
|
+
});
|
|
148
|
+
observer.on('parse-complete', () => {
|
|
149
|
+
this.importProps.phase = 'selecting';
|
|
150
|
+
});
|
|
89
151
|
return observer;
|
|
90
152
|
}
|
|
153
|
+
ingest(routes) {
|
|
154
|
+
const observer = new types_1.Observer();
|
|
155
|
+
if (!this.importProps)
|
|
156
|
+
this.prepare();
|
|
157
|
+
const list = this.getRouteList();
|
|
158
|
+
list.pauseListUpdates();
|
|
159
|
+
this.importProps.phase = 'ingesting';
|
|
160
|
+
this._ingest(routes, observer)
|
|
161
|
+
.catch(err => {
|
|
162
|
+
this.logError(err, 'ingest');
|
|
163
|
+
observer.emit('error', err.message);
|
|
164
|
+
})
|
|
165
|
+
.finally(() => {
|
|
166
|
+
list.resumeListUpdates();
|
|
167
|
+
list.emitLists('updated', { source: 'system' });
|
|
168
|
+
});
|
|
169
|
+
observer.on('ingest-progress', (progress) => {
|
|
170
|
+
const { current, total, currentName } = progress;
|
|
171
|
+
this.importProps.ingestProgress = { current, total, currentName };
|
|
172
|
+
});
|
|
173
|
+
observer.on('ingest-complete', (status) => {
|
|
174
|
+
this.importProps.phase = 'complete';
|
|
175
|
+
const { imported, skipped, errors, failedRoutes } = status;
|
|
176
|
+
this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
|
|
177
|
+
});
|
|
178
|
+
return observer;
|
|
179
|
+
}
|
|
180
|
+
cancel() {
|
|
181
|
+
this.isCancelled = true;
|
|
182
|
+
this.importProps.phase = 'landing';
|
|
183
|
+
}
|
|
184
|
+
async importRoute(fileInfo, observer) {
|
|
185
|
+
await (0, sleep_1.sleep)(0);
|
|
186
|
+
observer.emit('parsing');
|
|
187
|
+
this.importProps.phase = 'parsing';
|
|
188
|
+
if (fileInfo?.ext === 'gpx') {
|
|
189
|
+
return this.importSingleGpxRoute(fileInfo, observer);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
return this.importSingleVideoRoute(fileInfo, observer);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async importSingleGpxRoute(fileInfo, observer) {
|
|
196
|
+
const list = this.getRouteList();
|
|
197
|
+
const db = this.getRoutesDBLoader();
|
|
198
|
+
try {
|
|
199
|
+
const { data, details } = await parsers_1.RouteParser.parse(fileInfo);
|
|
200
|
+
const route = new route_1.Route(data, details);
|
|
201
|
+
route.description.tsImported = Date.now();
|
|
202
|
+
await db.save(route, true);
|
|
203
|
+
list.addRoute(route, 'user');
|
|
204
|
+
observer.emit('success', route);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
observer.emit('error', err.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async importSingleVideoRoute(fileInfo, observer) {
|
|
211
|
+
const parsers = this.getParsers();
|
|
212
|
+
const { dir, ext, delimiter } = fileInfo;
|
|
213
|
+
const folderUri = dir.endsWith(delimiter ?? '/') ? dir.slice(0, -delimiter.length) : dir;
|
|
214
|
+
try {
|
|
215
|
+
if (!parsers.isPrimaryExtension(ext)) {
|
|
216
|
+
observer.emit('error', 'not a route control file');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const scanObserver = new types_1.Observer();
|
|
220
|
+
await this.scanFolder(folderUri, folderUri, scanObserver, parsers, { scannedFolders: 0 }, { value: 0 }, false);
|
|
221
|
+
const files = this.scanResult;
|
|
222
|
+
this.scanResult = [];
|
|
223
|
+
scanObserver.stop();
|
|
224
|
+
if (files[0].scanError) {
|
|
225
|
+
observer.emit('error', files[0].scanError);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const file = files.find(file => file.controlFileUri.includes(fileInfo.base));
|
|
230
|
+
const parseObserver = this.parse([file]);
|
|
231
|
+
parseObserver.on('parse-result', (result) => {
|
|
232
|
+
parseObserver.stop();
|
|
233
|
+
if (result.parseError) {
|
|
234
|
+
observer.emit('error', result.parseError);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const ingest = this.ingest([result]);
|
|
238
|
+
let ingestError;
|
|
239
|
+
ingest.once('ingest-error', (_, reason) => {
|
|
240
|
+
ingest.stop();
|
|
241
|
+
observer.emit('error', reason);
|
|
242
|
+
ingestError = reason;
|
|
243
|
+
});
|
|
244
|
+
ingest.once('ingest-complete', (summary) => {
|
|
245
|
+
ingest.stop();
|
|
246
|
+
if (!ingestError && summary.imported > 0) {
|
|
247
|
+
observer.emit('success', summary.importedRoutes?.[0]?.title);
|
|
248
|
+
}
|
|
249
|
+
else if (!ingestError && summary.imported === 0) {
|
|
250
|
+
observer.emit('error', 'not imported');
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
observer.emit('error', err.message);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
91
260
|
async _scan(folderInfo, observer) {
|
|
92
261
|
await (0, utils_1.waitNextTick)();
|
|
93
|
-
const parsers =
|
|
262
|
+
const parsers = this.getParsers();
|
|
94
263
|
const progress = { scannedFolders: 0 };
|
|
95
264
|
const discoveredCount = { value: 0 };
|
|
96
265
|
await this.scanFolder(folderInfo.uri, folderInfo.displayName, observer, parsers, progress, discoveredCount);
|
|
97
266
|
await this.upsertImportHistory(folderInfo, discoveredCount.value);
|
|
98
|
-
observer.emit('scan-complete');
|
|
267
|
+
observer.emit('scan-complete', this.scanResult);
|
|
99
268
|
}
|
|
100
|
-
async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount) {
|
|
269
|
+
async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount, recursive = true) {
|
|
101
270
|
const fs = this.getBindings().fs;
|
|
102
271
|
let entries;
|
|
103
272
|
try {
|
|
104
|
-
entries = await fs.readdir(uri, { extended: true });
|
|
273
|
+
entries = await fs.readdir(uri, { recursive: false, extended: true });
|
|
105
274
|
}
|
|
106
275
|
catch (err) {
|
|
107
276
|
this.logError(err, 'scanFolder', { uri });
|
|
@@ -111,139 +280,153 @@ let RouteLibraryScannerService = (() => {
|
|
|
111
280
|
observer.emit('scan-progress', { scannedFolders: progress.scannedFolders });
|
|
112
281
|
const files = entries.filter(e => !e.isDirectory);
|
|
113
282
|
const dirs = entries.filter(e => e.isDirectory);
|
|
114
|
-
const primaryFiles = files.filter(f => {
|
|
115
|
-
const
|
|
283
|
+
const primaryFiles = files.filter((f) => {
|
|
284
|
+
const name = typeof (f) === 'string' ? f : f.name;
|
|
285
|
+
const ext = this.getExtension(name);
|
|
116
286
|
return ext && parsers.isPrimaryExtension(ext);
|
|
287
|
+
}).map((f) => {
|
|
288
|
+
if (typeof f === 'string') {
|
|
289
|
+
return {
|
|
290
|
+
name: f,
|
|
291
|
+
isDirectory: false,
|
|
292
|
+
uri: this.getBindings().path.join(uri, f)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
else
|
|
296
|
+
return f;
|
|
117
297
|
});
|
|
118
298
|
for (const file of primaryFiles) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
299
|
+
if (!this.isCancelled) {
|
|
300
|
+
const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
|
|
301
|
+
discoveredCount.value++;
|
|
302
|
+
observer.emit('scan-result', routeAnnouncement);
|
|
303
|
+
this.scanResult.push(routeAnnouncement);
|
|
304
|
+
}
|
|
122
305
|
}
|
|
123
306
|
for (const dir of dirs) {
|
|
124
|
-
|
|
307
|
+
if (!this.isCancelled && recursive) {
|
|
308
|
+
await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
|
|
309
|
+
}
|
|
125
310
|
}
|
|
126
311
|
}
|
|
127
312
|
async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
|
|
128
313
|
const ext = this.getExtension(controlFile.name);
|
|
129
314
|
const baseName = controlFile.name.slice(0, controlFile.name.length - ext.length - 1);
|
|
130
|
-
let importable = true;
|
|
131
315
|
let skipReason;
|
|
132
316
|
const companionExts = this.getCompanionExts(parsers, ext);
|
|
133
317
|
for (const compExt of companionExts) {
|
|
134
318
|
const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
135
319
|
f.name.toLowerCase().startsWith(baseName.toLowerCase()));
|
|
136
320
|
if (!hasCompanion) {
|
|
137
|
-
importable = false;
|
|
138
321
|
skipReason = `Missing companion file (.${compExt})`;
|
|
139
322
|
break;
|
|
140
323
|
}
|
|
141
324
|
}
|
|
142
|
-
let hasVideo = false;
|
|
143
|
-
const hasThumbnail = folderFiles.some(f => IMAGE_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
|
|
144
|
-
if (importable) {
|
|
145
|
-
const videoFiles = folderFiles.filter(f => VIDEO_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
|
|
146
|
-
const nonAviVideo = videoFiles.find(f => this.getExtension(f.name).toLowerCase() !== 'avi');
|
|
147
|
-
if (videoFiles.length === 0) {
|
|
148
|
-
importable = false;
|
|
149
|
-
skipReason = 'No video file found in folder';
|
|
150
|
-
}
|
|
151
|
-
else if (nonAviVideo) {
|
|
152
|
-
hasVideo = true;
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
importable = false;
|
|
156
|
-
skipReason = 'Only AVI video format found; AVI is not supported';
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (importable) {
|
|
160
|
-
try {
|
|
161
|
-
const fs = this.getBindings().fs;
|
|
162
|
-
const content = await fs.readFile(controlFile.uri);
|
|
163
|
-
const text = typeof content === 'string' ? content : content?.toString?.('utf8') ?? '';
|
|
164
|
-
if (this.containsAbsolutePath(text.slice(0, 4096))) {
|
|
165
|
-
importable = false;
|
|
166
|
-
skipReason = 'Route references an absolute video path';
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const alreadyImported = await this.getRouteList()
|
|
173
|
-
.existsBySourceUri(controlFile.uri)
|
|
174
|
-
.catch(() => false);
|
|
175
325
|
return {
|
|
176
|
-
id: (0, uuid_1.v4)(),
|
|
177
326
|
folderUri,
|
|
178
327
|
folderName,
|
|
328
|
+
files: folderFiles,
|
|
179
329
|
controlFileUri: controlFile.uri,
|
|
180
330
|
format: ext,
|
|
181
|
-
|
|
182
|
-
hasThumbnail,
|
|
183
|
-
alreadyImported,
|
|
184
|
-
importable,
|
|
185
|
-
skipReason
|
|
331
|
+
scanError: skipReason
|
|
186
332
|
};
|
|
187
333
|
}
|
|
188
|
-
async
|
|
334
|
+
async _parse(scannedRoutes, observer) {
|
|
335
|
+
const service = this.getRouteList();
|
|
336
|
+
const targets = scannedRoutes.filter(r => !r.scanError);
|
|
337
|
+
const total = targets.length;
|
|
338
|
+
for (let i = 0; i < targets.length; i++) {
|
|
339
|
+
if (this.isCancelled)
|
|
340
|
+
continue;
|
|
341
|
+
const parsed = i + 1;
|
|
342
|
+
const target = targets[i];
|
|
343
|
+
observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
|
|
344
|
+
await this._parseTarget(target, service, observer);
|
|
345
|
+
}
|
|
346
|
+
observer.emit('parse-complete');
|
|
347
|
+
}
|
|
348
|
+
async _parseTarget(target, service, observer) {
|
|
349
|
+
if (service.existsBySourceUri(target.controlFileUri)) {
|
|
350
|
+
observer.emit('parse-result', {
|
|
351
|
+
alreadyImported: true,
|
|
352
|
+
route: service.getBySourceUri(target.controlFileUri),
|
|
353
|
+
folderUri: target.folderUri,
|
|
354
|
+
controlFileUri: target.controlFileUri,
|
|
355
|
+
format: target.format
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let result;
|
|
360
|
+
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
361
|
+
try {
|
|
362
|
+
try {
|
|
363
|
+
result = await parsers_1.RouteParser.parse(file);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
throw new Error(`Could not parse: [${err.message}]`);
|
|
367
|
+
}
|
|
368
|
+
if (result.data.hasVideo) {
|
|
369
|
+
this.validateVideoUrl(result.details, target.folderUri, target.files);
|
|
370
|
+
}
|
|
371
|
+
observer.emit('parse-result', {
|
|
372
|
+
alreadyImported: false,
|
|
373
|
+
route: new route_1.Route(result.data, result.details),
|
|
374
|
+
folderUri: target.folderUri,
|
|
375
|
+
controlFileUri: target.controlFileUri,
|
|
376
|
+
format: target.format
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
observer.emit('parse-result', {
|
|
381
|
+
alreadyImported: false,
|
|
382
|
+
route: result ? new route_1.Route(result.data, result.details) : undefined,
|
|
383
|
+
folderUri: target.folderUri,
|
|
384
|
+
controlFileUri: target.controlFileUri,
|
|
385
|
+
format: target.format,
|
|
386
|
+
parseError: err?.message ?? String(err)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
validateVideoUrl(routeDetail, folderUri, folderFiles) {
|
|
391
|
+
if (this.isMobile()) {
|
|
392
|
+
if (routeDetail.video.format === 'avi') {
|
|
393
|
+
throw new Error('AVI video not supported');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (routeDetail.video.file) {
|
|
397
|
+
routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri, folderFiles);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async _ingest(routes, observer) {
|
|
189
401
|
await (0, utils_1.waitNextTick)();
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
402
|
+
const service = this.getRouteList();
|
|
403
|
+
const db = this.getRoutesDBLoader();
|
|
404
|
+
const target = routes.filter(r => !r.alreadyImported && !r.parseError);
|
|
405
|
+
const total = target.length;
|
|
193
406
|
let errors = 0;
|
|
194
407
|
const failedRoutes = [];
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
408
|
+
const importedRoutes = [];
|
|
409
|
+
for (let i = 0; i < target.length; i++) {
|
|
410
|
+
if (this.isCancelled)
|
|
411
|
+
continue;
|
|
412
|
+
const { route } = target[i] ?? {};
|
|
198
413
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
414
|
+
observer.emit('ingest-progress', { current: i + 1, total, currentName: route.title });
|
|
415
|
+
await db.save(route, true);
|
|
416
|
+
service.addRoute(route, 'user');
|
|
417
|
+
importedRoutes.push(route);
|
|
201
418
|
}
|
|
202
419
|
catch (err) {
|
|
203
420
|
const reason = err?.message ?? String(err);
|
|
204
421
|
errors++;
|
|
205
|
-
failedRoutes.push({ name: route.
|
|
206
|
-
observer.emit('ingest-error', { name: route.
|
|
422
|
+
failedRoutes.push({ name: route.title, reason });
|
|
423
|
+
observer.emit('ingest-error', { name: route.title, reason });
|
|
207
424
|
}
|
|
208
425
|
}
|
|
209
|
-
const skipped = routes.length -
|
|
210
|
-
observer.emit('ingest-complete', { imported, skipped, errors, failedRoutes });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const { format, controlFileUri, folderUri } = route;
|
|
214
|
-
const fileInfo = this.buildFileInfo(controlFileUri, format);
|
|
215
|
-
const { data } = await parsers_1.RouteParser.parse(fileInfo);
|
|
216
|
-
let thumbnailPath;
|
|
217
|
-
if (route.hasThumbnail) {
|
|
218
|
-
thumbnailPath = await this.copyThumbnail(route, folderUri).catch(() => undefined);
|
|
219
|
-
}
|
|
220
|
-
const videoUri = data.videoUrl ? this.resolveVideoUri(data.videoUrl, folderUri) : undefined;
|
|
221
|
-
const record = {
|
|
222
|
-
id: data.id ?? (0, uuid_1.v4)(),
|
|
223
|
-
name: data.title ?? route.folderName,
|
|
224
|
-
format,
|
|
225
|
-
thumbnailPath,
|
|
226
|
-
videoUri,
|
|
227
|
-
sourceTreeUri: treeUri
|
|
228
|
-
};
|
|
229
|
-
await this.getRouteList().addRoute(record);
|
|
230
|
-
}
|
|
231
|
-
async copyThumbnail(route, folderUri) {
|
|
232
|
-
const { fs, appInfo } = this.getBindings();
|
|
233
|
-
if (!fs || !appInfo)
|
|
234
|
-
return undefined;
|
|
235
|
-
const entries = await fs.readdir(folderUri, { extended: true });
|
|
236
|
-
const thumbnailFile = entries.find(e => !e.isDirectory && IMAGE_EXTENSIONS.has(this.getExtension(e.name).toLowerCase()));
|
|
237
|
-
if (!thumbnailFile)
|
|
238
|
-
return undefined;
|
|
239
|
-
const destDir = `${appInfo.getAppDir()}/thumbnails`;
|
|
240
|
-
await fs.ensureDir(destDir);
|
|
241
|
-
const srcContent = await fs.readFile(thumbnailFile.uri);
|
|
242
|
-
const destPath = `${destDir}/${route.id}.${this.getExtension(thumbnailFile.name)}`;
|
|
243
|
-
await fs.writeFile(destPath, srcContent);
|
|
244
|
-
return destPath;
|
|
245
|
-
}
|
|
246
|
-
resolveVideoUri(videoRef, folderUri) {
|
|
426
|
+
const skipped = routes.length - target.length;
|
|
427
|
+
observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
|
|
428
|
+
}
|
|
429
|
+
resolveVideoUri(videoRef, folderUri, folderFiles) {
|
|
247
430
|
if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
|
|
248
431
|
return videoRef;
|
|
249
432
|
}
|
|
@@ -251,9 +434,14 @@ let RouteLibraryScannerService = (() => {
|
|
|
251
434
|
return videoRef;
|
|
252
435
|
}
|
|
253
436
|
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
254
|
-
|
|
437
|
+
if (this.isMobile())
|
|
438
|
+
throw new Error('Absolute video path references are not supported');
|
|
439
|
+
return videoRef;
|
|
255
440
|
}
|
|
256
|
-
|
|
441
|
+
const match = folderFiles.find(f => f.name.toLowerCase() === videoRef.toLowerCase());
|
|
442
|
+
if (!match)
|
|
443
|
+
throw new Error(`Video file not found in folder: ${videoRef}`);
|
|
444
|
+
return match.uri;
|
|
257
445
|
}
|
|
258
446
|
async upsertImportHistory(folderInfo, routeCount) {
|
|
259
447
|
try {
|
|
@@ -292,6 +480,24 @@ let RouteLibraryScannerService = (() => {
|
|
|
292
480
|
delimiter: '/'
|
|
293
481
|
};
|
|
294
482
|
}
|
|
483
|
+
buildRouteDisplayItem(parsed) {
|
|
484
|
+
const { route, alreadyImported, parseError, format } = parsed;
|
|
485
|
+
const descr = route?.description ?? {};
|
|
486
|
+
const [C, U] = this.getUnitConversionShortcuts();
|
|
487
|
+
const distance = descr.distance === undefined ? undefined : {
|
|
488
|
+
value: C(descr.distance, 'distance', { digits: 1 }),
|
|
489
|
+
unit: U('distance')
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
id: route.description.id,
|
|
493
|
+
distance,
|
|
494
|
+
label: route.title,
|
|
495
|
+
alreadyImported,
|
|
496
|
+
importable: parseError == null,
|
|
497
|
+
format,
|
|
498
|
+
errorReason: parseError
|
|
499
|
+
};
|
|
500
|
+
}
|
|
295
501
|
getCompanionExts(parsers, primaryExt) {
|
|
296
502
|
try {
|
|
297
503
|
const matching = parsers.suppertsExtension(primaryExt);
|
|
@@ -306,12 +512,8 @@ let RouteLibraryScannerService = (() => {
|
|
|
306
512
|
const dot = filename.lastIndexOf('.');
|
|
307
513
|
return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
308
514
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return true;
|
|
312
|
-
if (/[A-Za-z]:[/\\]/m.test(content))
|
|
313
|
-
return true;
|
|
314
|
-
return false;
|
|
515
|
+
isMobile() {
|
|
516
|
+
return this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
315
517
|
}
|
|
316
518
|
getRouteList() {
|
|
317
519
|
return (0, service_2.useRouteList)();
|
|
@@ -319,6 +521,18 @@ let RouteLibraryScannerService = (() => {
|
|
|
319
521
|
getBindings() {
|
|
320
522
|
return (0, api_1.getBindings)();
|
|
321
523
|
}
|
|
524
|
+
getRoutesDBLoader() {
|
|
525
|
+
return (0, db_1.useRoutesDbLoader)();
|
|
526
|
+
}
|
|
527
|
+
getParsers() {
|
|
528
|
+
return (0, parsers_1.useParsers)();
|
|
529
|
+
}
|
|
530
|
+
getUnitConversionShortcuts() {
|
|
531
|
+
return this.getUnitConverter().getUnitConversionShortcuts();
|
|
532
|
+
}
|
|
533
|
+
getUnitConverter() {
|
|
534
|
+
return (0, i18n_1.useUnitConverter)();
|
|
535
|
+
}
|
|
322
536
|
};
|
|
323
537
|
return RouteLibraryScannerService = _classThis;
|
|
324
538
|
})();
|
|
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.RoutesDbLoader = void 0;
|
|
40
|
+
exports.useRoutesDbLoader = exports.RoutesDbLoader = void 0;
|
|
41
41
|
const api_1 = require("../../../api");
|
|
42
42
|
const types_1 = require("../../../base/types");
|
|
43
43
|
const observer_1 = require("../../../base/types/observer");
|
|
@@ -339,3 +339,5 @@ let RoutesDbLoader = (() => {
|
|
|
339
339
|
return RoutesDbLoader = _classThis;
|
|
340
340
|
})();
|
|
341
341
|
exports.RoutesDbLoader = RoutesDbLoader;
|
|
342
|
+
const useRoutesDbLoader = () => new RoutesDbLoader();
|
|
343
|
+
exports.useRoutesDbLoader = useRoutesDbLoader;
|
|
@@ -126,6 +126,7 @@ let RouteListService = (() => {
|
|
|
126
126
|
syncInfo;
|
|
127
127
|
currentView;
|
|
128
128
|
stats;
|
|
129
|
+
isListUpdatePaused = false;
|
|
129
130
|
constructor() {
|
|
130
131
|
super('RouteList');
|
|
131
132
|
this.myRoutes = new myroutes_1.MyRoutes('myRoutes', 'My Routes');
|
|
@@ -709,7 +710,15 @@ let RouteListService = (() => {
|
|
|
709
710
|
this.logError(err, 'import', info);
|
|
710
711
|
}
|
|
711
712
|
}
|
|
713
|
+
pauseListUpdates() {
|
|
714
|
+
this.isListUpdatePaused = true;
|
|
715
|
+
}
|
|
716
|
+
resumeListUpdates() {
|
|
717
|
+
this.isListUpdatePaused = false;
|
|
718
|
+
}
|
|
712
719
|
emitLists(event, props) {
|
|
720
|
+
if (this.isListUpdatePaused)
|
|
721
|
+
return;
|
|
713
722
|
try {
|
|
714
723
|
const { log, source = 'user' } = props ?? {};
|
|
715
724
|
if (this.currentView === 'grid' || this.currentView === 'list') {
|
|
@@ -791,6 +800,25 @@ let RouteListService = (() => {
|
|
|
791
800
|
return [];
|
|
792
801
|
}
|
|
793
802
|
}
|
|
803
|
+
existsBySourceUri(uri) {
|
|
804
|
+
try {
|
|
805
|
+
return this.routes.some(r => r.description?.sourceTreeUri === uri ||
|
|
806
|
+
r.description?.videoUrl === uri);
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
this.logError(err, 'existsBySourceUri', { uri });
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
getBySourceUri(uri) {
|
|
814
|
+
try {
|
|
815
|
+
return this.routes.find(r => r.description?.sourceTreeUri === uri ||
|
|
816
|
+
r.description?.videoUrl === uri);
|
|
817
|
+
}
|
|
818
|
+
catch (err) {
|
|
819
|
+
this.logError(err, 'getBySourceUri', { uri });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
794
822
|
addRoute(route, source = 'system') {
|
|
795
823
|
this.routes.push(route);
|
|
796
824
|
if (route.description?.isDeleted) {
|