incyclist-services 1.7.52 → 1.7.53
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 +322 -105
- 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 +322 -105
- 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 -10
- package/lib/types/routes/library/types.d.ts +30 -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,137 +280,150 @@ 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,
|
|
179
328
|
controlFileUri: controlFile.uri,
|
|
180
329
|
format: ext,
|
|
181
|
-
|
|
182
|
-
hasThumbnail,
|
|
183
|
-
alreadyImported,
|
|
184
|
-
importable,
|
|
185
|
-
skipReason
|
|
330
|
+
scanError: skipReason
|
|
186
331
|
};
|
|
187
332
|
}
|
|
188
|
-
async
|
|
333
|
+
async _parse(scannedRoutes, observer) {
|
|
334
|
+
const service = this.getRouteList();
|
|
335
|
+
const targets = scannedRoutes.filter(r => !r.scanError);
|
|
336
|
+
const total = targets.length;
|
|
337
|
+
for (let i = 0; i < targets.length; i++) {
|
|
338
|
+
if (this.isCancelled)
|
|
339
|
+
continue;
|
|
340
|
+
const parsed = i + 1;
|
|
341
|
+
const target = targets[i];
|
|
342
|
+
observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
|
|
343
|
+
await this._parseTarget(target, service, observer);
|
|
344
|
+
}
|
|
345
|
+
observer.emit('parse-complete');
|
|
346
|
+
}
|
|
347
|
+
async _parseTarget(target, service, observer) {
|
|
348
|
+
if (service.existsBySourceUri(target.controlFileUri)) {
|
|
349
|
+
observer.emit('parse-result', {
|
|
350
|
+
alreadyImported: true,
|
|
351
|
+
route: service.getBySourceUri(target.controlFileUri),
|
|
352
|
+
folderUri: target.folderUri,
|
|
353
|
+
controlFileUri: target.controlFileUri,
|
|
354
|
+
format: target.format
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let result;
|
|
359
|
+
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
360
|
+
try {
|
|
361
|
+
try {
|
|
362
|
+
result = await parsers_1.RouteParser.parse(file);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
throw new Error(`Could not parse: [${err.message}]`);
|
|
366
|
+
}
|
|
367
|
+
if (result.data.hasVideo) {
|
|
368
|
+
this.validateVideoUrl(result.details, target.folderUri);
|
|
369
|
+
}
|
|
370
|
+
observer.emit('parse-result', {
|
|
371
|
+
alreadyImported: false,
|
|
372
|
+
route: new route_1.Route(result.data, result.details),
|
|
373
|
+
folderUri: target.folderUri,
|
|
374
|
+
controlFileUri: target.controlFileUri,
|
|
375
|
+
format: target.format
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
observer.emit('parse-result', {
|
|
380
|
+
alreadyImported: false,
|
|
381
|
+
route: result ? new route_1.Route(result.data, result.details) : undefined,
|
|
382
|
+
folderUri: target.folderUri,
|
|
383
|
+
controlFileUri: target.controlFileUri,
|
|
384
|
+
format: target.format,
|
|
385
|
+
parseError: err?.message ?? String(err)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
validateVideoUrl(routeDetail, folderUri) {
|
|
390
|
+
if (this.isMobile()) {
|
|
391
|
+
if (routeDetail.video.format === 'avi') {
|
|
392
|
+
throw new Error('AVI video not supported');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (routeDetail.video.file) {
|
|
396
|
+
routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async _ingest(routes, observer) {
|
|
189
400
|
await (0, utils_1.waitNextTick)();
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
401
|
+
const service = this.getRouteList();
|
|
402
|
+
const db = this.getRoutesDBLoader();
|
|
403
|
+
const target = routes.filter(r => !r.alreadyImported && !r.parseError);
|
|
404
|
+
const total = target.length;
|
|
193
405
|
let errors = 0;
|
|
194
406
|
const failedRoutes = [];
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
407
|
+
const importedRoutes = [];
|
|
408
|
+
for (let i = 0; i < target.length; i++) {
|
|
409
|
+
if (this.isCancelled)
|
|
410
|
+
continue;
|
|
411
|
+
const { route } = target[i] ?? {};
|
|
198
412
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
413
|
+
observer.emit('ingest-progress', { current: i + 1, total, currentName: route.title });
|
|
414
|
+
await db.save(route, true);
|
|
415
|
+
service.addRoute(route, 'user');
|
|
416
|
+
importedRoutes.push(route);
|
|
201
417
|
}
|
|
202
418
|
catch (err) {
|
|
203
419
|
const reason = err?.message ?? String(err);
|
|
204
420
|
errors++;
|
|
205
|
-
failedRoutes.push({ name: route.
|
|
206
|
-
observer.emit('ingest-error', { name: route.
|
|
421
|
+
failedRoutes.push({ name: route.title, reason });
|
|
422
|
+
observer.emit('ingest-error', { name: route.title, reason });
|
|
207
423
|
}
|
|
208
424
|
}
|
|
209
|
-
const skipped = routes.length -
|
|
210
|
-
observer.emit('ingest-complete', { imported, skipped, errors, failedRoutes });
|
|
211
|
-
}
|
|
212
|
-
async ingestRoute(route, treeUri) {
|
|
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;
|
|
425
|
+
const skipped = routes.length - target.length;
|
|
426
|
+
observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
|
|
245
427
|
}
|
|
246
428
|
resolveVideoUri(videoRef, folderUri) {
|
|
247
429
|
if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
|
|
@@ -251,7 +433,9 @@ let RouteLibraryScannerService = (() => {
|
|
|
251
433
|
return videoRef;
|
|
252
434
|
}
|
|
253
435
|
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
254
|
-
|
|
436
|
+
if (this.isMobile())
|
|
437
|
+
throw new Error('Absolute video path references are not supported during ingest');
|
|
438
|
+
return videoRef;
|
|
255
439
|
}
|
|
256
440
|
return `${folderUri}/${videoRef}`;
|
|
257
441
|
}
|
|
@@ -292,6 +476,24 @@ let RouteLibraryScannerService = (() => {
|
|
|
292
476
|
delimiter: '/'
|
|
293
477
|
};
|
|
294
478
|
}
|
|
479
|
+
buildRouteDisplayItem(parsed) {
|
|
480
|
+
const { route, alreadyImported, parseError, format } = parsed;
|
|
481
|
+
const descr = route?.description ?? {};
|
|
482
|
+
const [C, U] = this.getUnitConversionShortcuts();
|
|
483
|
+
const distance = descr.distance === undefined ? undefined : {
|
|
484
|
+
value: C(descr.distance, 'distance', { digits: 1 }),
|
|
485
|
+
unit: U('distance')
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
id: route.description.id,
|
|
489
|
+
distance,
|
|
490
|
+
label: route.title,
|
|
491
|
+
alreadyImported,
|
|
492
|
+
importable: parseError != null,
|
|
493
|
+
format,
|
|
494
|
+
errorReason: parseError
|
|
495
|
+
};
|
|
496
|
+
}
|
|
295
497
|
getCompanionExts(parsers, primaryExt) {
|
|
296
498
|
try {
|
|
297
499
|
const matching = parsers.suppertsExtension(primaryExt);
|
|
@@ -313,12 +515,27 @@ let RouteLibraryScannerService = (() => {
|
|
|
313
515
|
return true;
|
|
314
516
|
return false;
|
|
315
517
|
}
|
|
518
|
+
isMobile() {
|
|
519
|
+
return this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
520
|
+
}
|
|
316
521
|
getRouteList() {
|
|
317
522
|
return (0, service_2.useRouteList)();
|
|
318
523
|
}
|
|
319
524
|
getBindings() {
|
|
320
525
|
return (0, api_1.getBindings)();
|
|
321
526
|
}
|
|
527
|
+
getRoutesDBLoader() {
|
|
528
|
+
return (0, db_1.useRoutesDbLoader)();
|
|
529
|
+
}
|
|
530
|
+
getParsers() {
|
|
531
|
+
return (0, parsers_1.useParsers)();
|
|
532
|
+
}
|
|
533
|
+
getUnitConversionShortcuts() {
|
|
534
|
+
return this.getUnitConverter().getUnitConversionShortcuts();
|
|
535
|
+
}
|
|
536
|
+
getUnitConverter() {
|
|
537
|
+
return (0, i18n_1.useUnitConverter)();
|
|
538
|
+
}
|
|
322
539
|
};
|
|
323
540
|
return RouteLibraryScannerService = _classThis;
|
|
324
541
|
})();
|
|
@@ -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) {
|