incyclist-services 1.7.51 → 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/base/parsers/epm.js +6 -0
- package/lib/cjs/routes/base/parsers/factory.js +5 -0
- package/lib/cjs/routes/base/parsers/geometry.js +6 -0
- package/lib/cjs/routes/base/parsers/gpx.js +6 -0
- package/lib/cjs/routes/base/parsers/multixml.js +6 -0
- package/lib/cjs/routes/base/parsers/tacx/TacxParser.js +6 -0
- package/lib/cjs/routes/base/parsers/xml.js +6 -0
- package/lib/cjs/routes/library/service.js +544 -0
- package/lib/cjs/routes/library/types.js +2 -0
- 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/base/parsers/epm.js +6 -0
- package/lib/esm/routes/base/parsers/factory.js +5 -0
- package/lib/esm/routes/base/parsers/geometry.js +6 -0
- package/lib/esm/routes/base/parsers/gpx.js +6 -0
- package/lib/esm/routes/base/parsers/multixml.js +6 -0
- package/lib/esm/routes/base/parsers/tacx/TacxParser.js +6 -0
- package/lib/esm/routes/base/parsers/xml.js +6 -0
- package/lib/esm/routes/library/service.js +540 -0
- package/lib/esm/routes/library/types.js +1 -0
- 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/api/fs/index.d.ts +12 -1
- package/lib/types/api/ui/index.d.ts +1 -0
- package/lib/types/routes/base/parsers/epm.d.ts +2 -0
- package/lib/types/routes/base/parsers/factory.d.ts +2 -1
- package/lib/types/routes/base/parsers/geometry.d.ts +4 -1
- package/lib/types/routes/base/parsers/gpx.d.ts +2 -0
- package/lib/types/routes/base/parsers/index.d.ts +1 -1
- package/lib/types/routes/base/parsers/multixml.d.ts +3 -1
- package/lib/types/routes/base/parsers/tacx/TacxParser.d.ts +4 -1
- package/lib/types/routes/base/parsers/types.d.ts +14 -0
- package/lib/types/routes/base/parsers/xml.d.ts +4 -1
- package/lib/types/routes/base/types/index.d.ts +1 -11
- package/lib/types/routes/library/service.d.ts +44 -0
- package/lib/types/routes/library/types.d.ts +68 -0
- 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/lib/types/routes/types.d.ts +2 -0
- 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();
|
|
@@ -10,6 +10,12 @@ class EPMParser extends xml_1.XMLParser {
|
|
|
10
10
|
supportsExtension(extension) {
|
|
11
11
|
return extension.toLowerCase() === 'epm';
|
|
12
12
|
}
|
|
13
|
+
getPrimaryExtension() {
|
|
14
|
+
return 'epm';
|
|
15
|
+
}
|
|
16
|
+
getCompanionExtensions() {
|
|
17
|
+
return ['epp'];
|
|
18
|
+
}
|
|
13
19
|
async loadPoints(context) {
|
|
14
20
|
const { data, route } = context;
|
|
15
21
|
route.points = [];
|
|
@@ -24,6 +24,11 @@ class ParserFactory {
|
|
|
24
24
|
throw new Error(`invalid file format ${extension}`);
|
|
25
25
|
return matching;
|
|
26
26
|
}
|
|
27
|
+
isPrimaryExtension(extension) {
|
|
28
|
+
const ext = extension.toLowerCase();
|
|
29
|
+
const isPrimary = this.parsers.some(p => p.getPrimaryExtension() === ext);
|
|
30
|
+
return isPrimary;
|
|
31
|
+
}
|
|
27
32
|
findMatching(extension, data) {
|
|
28
33
|
const matching = this.parsers
|
|
29
34
|
.filter(p => p.supportsExtension(extension))
|
|
@@ -52,6 +52,12 @@ let GeometryParser = (() => {
|
|
|
52
52
|
const geometry = await this.getData(file, data);
|
|
53
53
|
return await this.parse(file, geometry);
|
|
54
54
|
}
|
|
55
|
+
getPrimaryExtension() {
|
|
56
|
+
return 'xml';
|
|
57
|
+
}
|
|
58
|
+
getCompanionExtensions() {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
55
61
|
supportsExtension(extension) {
|
|
56
62
|
return extension.toLowerCase() === 'json';
|
|
57
63
|
}
|
|
@@ -14,6 +14,12 @@ class GPXParser extends xml_1.XMLParser {
|
|
|
14
14
|
super();
|
|
15
15
|
this.props = props;
|
|
16
16
|
}
|
|
17
|
+
getPrimaryExtension() {
|
|
18
|
+
return 'gpx';
|
|
19
|
+
}
|
|
20
|
+
getCompanionExtensions() {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
17
23
|
supportsExtension(extension) {
|
|
18
24
|
return extension.toLowerCase() === 'gpx';
|
|
19
25
|
}
|
|
@@ -20,6 +20,12 @@ class MultipleXMLParser {
|
|
|
20
20
|
supportsExtension(extension) {
|
|
21
21
|
return extension?.toLowerCase() === 'xml';
|
|
22
22
|
}
|
|
23
|
+
getPrimaryExtension() {
|
|
24
|
+
return 'xml';
|
|
25
|
+
}
|
|
26
|
+
getCompanionExtensions() {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
23
29
|
supportsContent() {
|
|
24
30
|
return true;
|
|
25
31
|
}
|
|
@@ -39,6 +39,12 @@ class TacxParser {
|
|
|
39
39
|
supportsContent(data) {
|
|
40
40
|
return TacxReader_1.TacxFileReader.isValid(data);
|
|
41
41
|
}
|
|
42
|
+
getPrimaryExtension() {
|
|
43
|
+
return 'rlv';
|
|
44
|
+
}
|
|
45
|
+
getCompanionExtensions() {
|
|
46
|
+
return ['pgmf'];
|
|
47
|
+
}
|
|
42
48
|
buildContext(file) {
|
|
43
49
|
const { dir, delimiter: d } = file;
|
|
44
50
|
if (file.ext === 'rlv') {
|
|
@@ -18,6 +18,12 @@ class XMLParser {
|
|
|
18
18
|
const C = this.constructor;
|
|
19
19
|
return C['SCHEME'];
|
|
20
20
|
}
|
|
21
|
+
getPrimaryExtension() {
|
|
22
|
+
return 'xml';
|
|
23
|
+
}
|
|
24
|
+
getCompanionExtensions() {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
21
27
|
supportsExtension(extension) {
|
|
22
28
|
return extension.toLowerCase() === 'xml';
|
|
23
29
|
}
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
3
|
+
var useValue = arguments.length > 2;
|
|
4
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
5
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
6
|
+
}
|
|
7
|
+
return useValue ? value : void 0;
|
|
8
|
+
};
|
|
9
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
10
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
11
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
12
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
13
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
14
|
+
var _, done = false;
|
|
15
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
16
|
+
var context = {};
|
|
17
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
18
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
19
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
20
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
21
|
+
if (kind === "accessor") {
|
|
22
|
+
if (result === void 0) continue;
|
|
23
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
24
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
25
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
26
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
27
|
+
}
|
|
28
|
+
else if (_ = accept(result)) {
|
|
29
|
+
if (kind === "field") initializers.unshift(_);
|
|
30
|
+
else descriptor[key] = _;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
34
|
+
done = true;
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.useRouteLibraryScanner = exports.RouteLibraryScannerService = void 0;
|
|
38
|
+
const uuid_1 = require("uuid");
|
|
39
|
+
const api_1 = require("../../api");
|
|
40
|
+
const json_1 = require("../../api/repository/json");
|
|
41
|
+
const decorators_1 = require("../../base/decorators");
|
|
42
|
+
const service_1 = require("../../base/service");
|
|
43
|
+
const types_1 = require("../../base/types");
|
|
44
|
+
const parsers_1 = require("../base/parsers");
|
|
45
|
+
const service_2 = require("../list/service");
|
|
46
|
+
const utils_1 = require("../../utils");
|
|
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");
|
|
51
|
+
let RouteLibraryScannerService = (() => {
|
|
52
|
+
let _classDecorators = [decorators_1.Singleton];
|
|
53
|
+
let _classDescriptor;
|
|
54
|
+
let _classExtraInitializers = [];
|
|
55
|
+
let _classThis;
|
|
56
|
+
let _classSuper = service_1.IncyclistService;
|
|
57
|
+
let _instanceExtraInitializers = [];
|
|
58
|
+
let _getRouteList_decorators;
|
|
59
|
+
let _getBindings_decorators;
|
|
60
|
+
let _getRoutesDBLoader_decorators;
|
|
61
|
+
let _getParsers_decorators;
|
|
62
|
+
let _getUnitConverter_decorators;
|
|
63
|
+
var RouteLibraryScannerService = class extends _classSuper {
|
|
64
|
+
static { _classThis = this; }
|
|
65
|
+
static {
|
|
66
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
67
|
+
_getRouteList_decorators = [decorators_1.Injectable];
|
|
68
|
+
_getBindings_decorators = [decorators_1.Injectable];
|
|
69
|
+
_getRoutesDBLoader_decorators = [decorators_1.Injectable];
|
|
70
|
+
_getParsers_decorators = [decorators_1.Injectable];
|
|
71
|
+
_getUnitConverter_decorators = [decorators_1.Injectable];
|
|
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);
|
|
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);
|
|
77
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
78
|
+
RouteLibraryScannerService = _classThis = _classDescriptor.value;
|
|
79
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
80
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
81
|
+
}
|
|
82
|
+
isCancelled = (__runInitializers(this, _instanceExtraInitializers), false);
|
|
83
|
+
scanResult = [];
|
|
84
|
+
importProps;
|
|
85
|
+
constructor() {
|
|
86
|
+
super('RouteLibraryScanner');
|
|
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;
|
|
116
|
+
}
|
|
117
|
+
scan(folderInfo) {
|
|
118
|
+
if (!this.importProps)
|
|
119
|
+
this.prepare();
|
|
120
|
+
this.isCancelled = false;
|
|
121
|
+
this.scanResult = [];
|
|
122
|
+
this.importProps.phase = 'scanning';
|
|
123
|
+
const observer = new types_1.Observer();
|
|
124
|
+
this._scan(folderInfo, observer).catch(err => {
|
|
125
|
+
this.logError(err, 'scan', { uri: folderInfo.uri });
|
|
126
|
+
observer.emit('error', err.message);
|
|
127
|
+
});
|
|
128
|
+
observer.on('scan-progress', (progress) => {
|
|
129
|
+
this.importProps.scanProgress = progress;
|
|
130
|
+
});
|
|
131
|
+
return observer;
|
|
132
|
+
}
|
|
133
|
+
parse(scannedRoutes) {
|
|
134
|
+
const observer = new types_1.Observer();
|
|
135
|
+
if (!this.importProps)
|
|
136
|
+
this.prepare();
|
|
137
|
+
this._parse(scannedRoutes, observer).catch(err => {
|
|
138
|
+
this.logError(err, 'parse');
|
|
139
|
+
observer.emit('error', err.message);
|
|
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
|
+
});
|
|
151
|
+
return observer;
|
|
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
|
+
}
|
|
260
|
+
async _scan(folderInfo, observer) {
|
|
261
|
+
await (0, utils_1.waitNextTick)();
|
|
262
|
+
const parsers = this.getParsers();
|
|
263
|
+
const progress = { scannedFolders: 0 };
|
|
264
|
+
const discoveredCount = { value: 0 };
|
|
265
|
+
await this.scanFolder(folderInfo.uri, folderInfo.displayName, observer, parsers, progress, discoveredCount);
|
|
266
|
+
await this.upsertImportHistory(folderInfo, discoveredCount.value);
|
|
267
|
+
observer.emit('scan-complete', this.scanResult);
|
|
268
|
+
}
|
|
269
|
+
async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount, recursive = true) {
|
|
270
|
+
const fs = this.getBindings().fs;
|
|
271
|
+
let entries;
|
|
272
|
+
try {
|
|
273
|
+
entries = await fs.readdir(uri, { recursive: false, extended: true });
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
this.logError(err, 'scanFolder', { uri });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
progress.scannedFolders++;
|
|
280
|
+
observer.emit('scan-progress', { scannedFolders: progress.scannedFolders });
|
|
281
|
+
const files = entries.filter(e => !e.isDirectory);
|
|
282
|
+
const dirs = entries.filter(e => e.isDirectory);
|
|
283
|
+
const primaryFiles = files.filter((f) => {
|
|
284
|
+
const name = typeof (f) === 'string' ? f : f.name;
|
|
285
|
+
const ext = this.getExtension(name);
|
|
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;
|
|
297
|
+
});
|
|
298
|
+
for (const file of primaryFiles) {
|
|
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
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const dir of dirs) {
|
|
307
|
+
if (!this.isCancelled && recursive) {
|
|
308
|
+
await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
|
|
313
|
+
const ext = this.getExtension(controlFile.name);
|
|
314
|
+
const baseName = controlFile.name.slice(0, controlFile.name.length - ext.length - 1);
|
|
315
|
+
let skipReason;
|
|
316
|
+
const companionExts = this.getCompanionExts(parsers, ext);
|
|
317
|
+
for (const compExt of companionExts) {
|
|
318
|
+
const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
319
|
+
f.name.toLowerCase().startsWith(baseName.toLowerCase()));
|
|
320
|
+
if (!hasCompanion) {
|
|
321
|
+
skipReason = `Missing companion file (.${compExt})`;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
folderUri,
|
|
327
|
+
folderName,
|
|
328
|
+
controlFileUri: controlFile.uri,
|
|
329
|
+
format: ext,
|
|
330
|
+
scanError: skipReason
|
|
331
|
+
};
|
|
332
|
+
}
|
|
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) {
|
|
400
|
+
await (0, utils_1.waitNextTick)();
|
|
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;
|
|
405
|
+
let errors = 0;
|
|
406
|
+
const failedRoutes = [];
|
|
407
|
+
const importedRoutes = [];
|
|
408
|
+
for (let i = 0; i < target.length; i++) {
|
|
409
|
+
if (this.isCancelled)
|
|
410
|
+
continue;
|
|
411
|
+
const { route } = target[i] ?? {};
|
|
412
|
+
try {
|
|
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);
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const reason = err?.message ?? String(err);
|
|
420
|
+
errors++;
|
|
421
|
+
failedRoutes.push({ name: route.title, reason });
|
|
422
|
+
observer.emit('ingest-error', { name: route.title, reason });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const skipped = routes.length - target.length;
|
|
426
|
+
observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
|
|
427
|
+
}
|
|
428
|
+
resolveVideoUri(videoRef, folderUri) {
|
|
429
|
+
if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
|
|
430
|
+
return videoRef;
|
|
431
|
+
}
|
|
432
|
+
if (videoRef.startsWith('content://')) {
|
|
433
|
+
return videoRef;
|
|
434
|
+
}
|
|
435
|
+
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
436
|
+
if (this.isMobile())
|
|
437
|
+
throw new Error('Absolute video path references are not supported during ingest');
|
|
438
|
+
return videoRef;
|
|
439
|
+
}
|
|
440
|
+
return `${folderUri}/${videoRef}`;
|
|
441
|
+
}
|
|
442
|
+
async upsertImportHistory(folderInfo, routeCount) {
|
|
443
|
+
try {
|
|
444
|
+
const repo = json_1.JsonRepository.create('importedLibraries');
|
|
445
|
+
const names = await repo.list();
|
|
446
|
+
const all = await Promise.all((names ?? []).map(n => repo.read(n)));
|
|
447
|
+
const existing = all
|
|
448
|
+
.map(lib => lib)
|
|
449
|
+
.find(lib => lib?.treeUri === folderInfo.uri);
|
|
450
|
+
const id = existing?.id ?? (0, uuid_1.v4)();
|
|
451
|
+
await repo.write(id, {
|
|
452
|
+
id,
|
|
453
|
+
treeUri: folderInfo.uri,
|
|
454
|
+
displayName: folderInfo.displayName,
|
|
455
|
+
lastScanned: new Date().toISOString(),
|
|
456
|
+
routeCount
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
this.logError(err, 'upsertImportHistory', { uri: folderInfo.uri });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
buildFileInfo(uri, ext) {
|
|
464
|
+
const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\'));
|
|
465
|
+
const dir = uri.slice(0, lastSlash);
|
|
466
|
+
const base = uri.slice(lastSlash + 1);
|
|
467
|
+
const name = base.slice(0, base.length - ext.length - 1);
|
|
468
|
+
return {
|
|
469
|
+
type: 'url',
|
|
470
|
+
url: uri,
|
|
471
|
+
filename: uri,
|
|
472
|
+
base,
|
|
473
|
+
name,
|
|
474
|
+
dir,
|
|
475
|
+
ext,
|
|
476
|
+
delimiter: '/'
|
|
477
|
+
};
|
|
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
|
+
}
|
|
497
|
+
getCompanionExts(parsers, primaryExt) {
|
|
498
|
+
try {
|
|
499
|
+
const matching = parsers.suppertsExtension(primaryExt);
|
|
500
|
+
const parser = matching.find(p => p.getPrimaryExtension() === primaryExt);
|
|
501
|
+
return parser?.getCompanionExtensions() ?? [];
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
getExtension(filename) {
|
|
508
|
+
const dot = filename.lastIndexOf('.');
|
|
509
|
+
return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
510
|
+
}
|
|
511
|
+
containsAbsolutePath(content) {
|
|
512
|
+
if (/[>"']\//m.test(content))
|
|
513
|
+
return true;
|
|
514
|
+
if (/[A-Za-z]:[/\\]/m.test(content))
|
|
515
|
+
return true;
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
isMobile() {
|
|
519
|
+
return this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
520
|
+
}
|
|
521
|
+
getRouteList() {
|
|
522
|
+
return (0, service_2.useRouteList)();
|
|
523
|
+
}
|
|
524
|
+
getBindings() {
|
|
525
|
+
return (0, api_1.getBindings)();
|
|
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
|
+
}
|
|
539
|
+
};
|
|
540
|
+
return RouteLibraryScannerService = _classThis;
|
|
541
|
+
})();
|
|
542
|
+
exports.RouteLibraryScannerService = RouteLibraryScannerService;
|
|
543
|
+
const useRouteLibraryScanner = () => new RouteLibraryScannerService();
|
|
544
|
+
exports.useRouteLibraryScanner = useRouteLibraryScanner;
|
|
@@ -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;
|