incyclist-services 1.8.0 → 1.8.2
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/activities/ride/service.js +17 -3
- package/lib/cjs/routes/base/api/index.js +6 -2
- package/lib/cjs/routes/download/service.js +8 -0
- package/lib/cjs/routes/index.js +2 -0
- package/lib/cjs/routes/library/errors.js +52 -0
- package/lib/cjs/routes/library/index.js +18 -0
- package/lib/cjs/routes/library/service.js +390 -191
- package/lib/cjs/routes/list/cards/RouteCard.js +19 -5
- package/lib/cjs/routes/list/detailsQueue.js +74 -0
- package/lib/cjs/routes/list/lists/selected.js +3 -2
- package/lib/cjs/routes/list/service.js +229 -61
- package/lib/cjs/routes/shapes/index.js +21 -0
- package/lib/cjs/routes/shapes/store.js +163 -0
- package/lib/cjs/routes/shapes/types.js +2 -0
- package/lib/cjs/routes/shapes/utils.js +89 -0
- package/lib/esm/activities/ride/service.js +17 -3
- package/lib/esm/routes/base/api/index.js +6 -2
- package/lib/esm/routes/download/service.js +8 -0
- package/lib/esm/routes/index.js +2 -0
- package/lib/esm/routes/library/errors.js +46 -0
- package/lib/esm/routes/library/index.js +2 -0
- package/lib/esm/routes/library/service.js +390 -191
- package/lib/esm/routes/list/cards/RouteCard.js +19 -5
- package/lib/esm/routes/list/detailsQueue.js +70 -0
- package/lib/esm/routes/list/lists/selected.js +1 -1
- package/lib/esm/routes/list/service.js +230 -62
- package/lib/esm/routes/shapes/index.js +2 -0
- package/lib/esm/routes/shapes/store.js +159 -0
- package/lib/esm/routes/shapes/types.js +1 -0
- package/lib/esm/routes/shapes/utils.js +82 -0
- package/lib/types/activities/ride/service.d.ts +2 -0
- package/lib/types/routes/download/service.d.ts +1 -0
- package/lib/types/routes/index.d.ts +2 -0
- package/lib/types/routes/library/errors.d.ts +12 -0
- package/lib/types/routes/library/index.d.ts +3 -0
- package/lib/types/routes/library/service.d.ts +32 -4
- package/lib/types/routes/library/types.d.ts +31 -1
- package/lib/types/routes/list/cards/RouteCard.d.ts +1 -0
- package/lib/types/routes/list/cards/types.d.ts +2 -0
- package/lib/types/routes/list/detailsQueue.d.ts +16 -0
- package/lib/types/routes/list/lists/selected.d.ts +2 -0
- package/lib/types/routes/list/service.d.ts +45 -7
- package/lib/types/routes/list/types.d.ts +1 -0
- package/lib/types/routes/shapes/index.d.ts +3 -0
- package/lib/types/routes/shapes/store.d.ts +22 -0
- package/lib/types/routes/shapes/types.d.ts +12 -0
- package/lib/types/routes/shapes/utils.d.ts +8 -0
- package/package.json +5 -5
|
@@ -44,6 +44,7 @@ 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 errors_1 = require("./errors");
|
|
47
48
|
const db_1 = require("../list/loaders/db");
|
|
48
49
|
const route_1 = require("../base/model/route");
|
|
49
50
|
const sleep_1 = require("../../utils/sleep");
|
|
@@ -51,7 +52,11 @@ const i18n_1 = require("../../i18n");
|
|
|
51
52
|
const utils_2 = require("../base/parsers/utils");
|
|
52
53
|
const externalFiles_1 = require("../../fileaccess/externalFiles");
|
|
53
54
|
const store_1 = require("../previews/store");
|
|
55
|
+
const store_2 = require("../shapes/store");
|
|
54
56
|
const WAITING_FOR_ICLOUD_THRESHOLD_MS = 2_000;
|
|
57
|
+
const VIDEO_EXTENSIONS = ['mp4', 'avi', 'mov', 'm4v', 'mkv', 'webm', 'mpg', 'mpeg', 'wmv'];
|
|
58
|
+
const PATH_SEPARATOR = /[/\\]/;
|
|
59
|
+
const UNREADABLE_FOLDER_ERRORS = /\b(EACCES|EPERM|EIO|EBUSY|EAGAIN|ETIMEDOUT|ESTALE|EMFILE|ENFILE|EHOSTDOWN|EHOSTUNREACH|ENETDOWN|ENETUNREACH|ECONNRESET|ECONNABORTED)\b/;
|
|
55
60
|
let RouteLibraryScannerService = (() => {
|
|
56
61
|
let _classDecorators = [decorators_1.Singleton];
|
|
57
62
|
let _classDescriptor;
|
|
@@ -63,6 +68,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
63
68
|
let _getBindings_decorators;
|
|
64
69
|
let _getRoutesDBLoader_decorators;
|
|
65
70
|
let _getPreviewStore_decorators;
|
|
71
|
+
let _getRouteShapeStore_decorators;
|
|
66
72
|
let _getParsers_decorators;
|
|
67
73
|
let _getUnitConverter_decorators;
|
|
68
74
|
var RouteLibraryScannerService = class extends _classSuper {
|
|
@@ -73,12 +79,14 @@ let RouteLibraryScannerService = (() => {
|
|
|
73
79
|
_getBindings_decorators = [decorators_1.Injectable];
|
|
74
80
|
_getRoutesDBLoader_decorators = [decorators_1.Injectable];
|
|
75
81
|
_getPreviewStore_decorators = [decorators_1.Injectable];
|
|
82
|
+
_getRouteShapeStore_decorators = [decorators_1.Injectable];
|
|
76
83
|
_getParsers_decorators = [decorators_1.Injectable];
|
|
77
84
|
_getUnitConverter_decorators = [decorators_1.Injectable];
|
|
78
85
|
__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);
|
|
79
86
|
__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);
|
|
80
87
|
__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);
|
|
81
88
|
__esDecorate(this, null, _getPreviewStore_decorators, { kind: "method", name: "getPreviewStore", static: false, private: false, access: { has: obj => "getPreviewStore" in obj, get: obj => obj.getPreviewStore }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
89
|
+
__esDecorate(this, null, _getRouteShapeStore_decorators, { kind: "method", name: "getRouteShapeStore", static: false, private: false, access: { has: obj => "getRouteShapeStore" in obj, get: obj => obj.getRouteShapeStore }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
82
90
|
__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);
|
|
83
91
|
__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);
|
|
84
92
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
@@ -118,35 +126,89 @@ let RouteLibraryScannerService = (() => {
|
|
|
118
126
|
importSingle(fileInfo) {
|
|
119
127
|
const observer = new types_1.Observer();
|
|
120
128
|
this.isCancelled = false;
|
|
121
|
-
this.
|
|
129
|
+
if (!this.importProps)
|
|
130
|
+
this.prepare();
|
|
131
|
+
const session = this.importProps;
|
|
132
|
+
delete session.error;
|
|
133
|
+
delete session.failure;
|
|
134
|
+
delete session.resultSuccess;
|
|
135
|
+
this.importRoute(fileInfo, observer, session).catch(err => {
|
|
122
136
|
this.logError(err, 'importSingle', { file: fileInfo?.filename });
|
|
123
137
|
observer.emit('error', err.message);
|
|
124
138
|
});
|
|
125
139
|
observer.on('success', (route) => {
|
|
126
|
-
if (
|
|
140
|
+
if (this.importProps !== session)
|
|
141
|
+
return;
|
|
142
|
+
session.phase = 'result';
|
|
143
|
+
session.resultSuccess = { routeName: route.title };
|
|
144
|
+
});
|
|
145
|
+
observer.on('error', (error, failure) => {
|
|
146
|
+
if (this.importProps !== session)
|
|
127
147
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
session.phase = 'result';
|
|
149
|
+
session.error = error;
|
|
150
|
+
session.failure = failure;
|
|
151
|
+
});
|
|
152
|
+
return observer;
|
|
153
|
+
}
|
|
154
|
+
importFiles(files) {
|
|
155
|
+
const observer = new types_1.Observer();
|
|
156
|
+
this.isCancelled = false;
|
|
157
|
+
if (!this.importProps)
|
|
158
|
+
this.prepare();
|
|
159
|
+
this.importProps.phase = 'ingesting';
|
|
160
|
+
const list = this.getRouteList();
|
|
161
|
+
list.pauseListUpdates();
|
|
162
|
+
this._importFiles(files ?? [], observer)
|
|
163
|
+
.catch(err => {
|
|
164
|
+
this.logError(err, 'importFiles');
|
|
165
|
+
observer.emit('error', err.message);
|
|
166
|
+
})
|
|
167
|
+
.finally(() => {
|
|
168
|
+
list.resumeListUpdates();
|
|
169
|
+
list.emitLists('updated', { source: 'system' });
|
|
130
170
|
});
|
|
131
|
-
observer.on('
|
|
171
|
+
observer.on('ingest-progress', (progress) => {
|
|
172
|
+
if (this.importProps)
|
|
173
|
+
this.importProps.ingestProgress = progress;
|
|
174
|
+
});
|
|
175
|
+
observer.on('ingest-complete', (status) => {
|
|
132
176
|
if (!this.importProps)
|
|
133
177
|
return;
|
|
134
|
-
|
|
135
|
-
this.importProps.
|
|
178
|
+
const { imported, skipped, errors, failedRoutes } = status;
|
|
179
|
+
this.importProps.phase = 'complete';
|
|
180
|
+
this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
|
|
136
181
|
});
|
|
137
182
|
return observer;
|
|
138
183
|
}
|
|
184
|
+
async importRouteFile(fileInfo, props) {
|
|
185
|
+
const name = fileInfo?.url ?? fileInfo?.filename ?? fileInfo?.name;
|
|
186
|
+
this.logEvent({ message: 'import single route file', file: name, type: fileInfo?.ext });
|
|
187
|
+
try {
|
|
188
|
+
const route = await this.parseRouteFile(fileInfo, props?.isCancelled);
|
|
189
|
+
this.logEvent({ message: 'import single route file success', file: name });
|
|
190
|
+
const list = props?.list ?? this.getRouteList();
|
|
191
|
+
const existing = list.findCard(route) ?? null;
|
|
192
|
+
await this.ingestOne(route, existing, list, this.getRoutesDBLoader());
|
|
193
|
+
return route;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const error = (0, errors_1.toRouteImportError)(err);
|
|
197
|
+
this.logEvent({ message: 'import single route file failed', file: name, reason: error.message, code: error.code, stack: err?.stack });
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
139
201
|
scan(folderInfo) {
|
|
140
202
|
if (!this.importProps)
|
|
141
203
|
this.prepare();
|
|
142
|
-
this.logEvent({ message: 'import route library', folder: folderInfo.displayName
|
|
204
|
+
this.logEvent({ message: 'import route library', folder: folderInfo.displayName });
|
|
143
205
|
this.isCancelled = false;
|
|
144
206
|
this.scanResult = [];
|
|
145
207
|
this.importProps.phase = 'scanning';
|
|
146
|
-
this.importProps.scanProgress = { scannedFolders: 0 };
|
|
208
|
+
this.importProps.scanProgress = { scannedFolders: 0, failedFolders: 0 };
|
|
147
209
|
const observer = new types_1.Observer();
|
|
148
210
|
this._scan(folderInfo, observer).catch(err => {
|
|
149
|
-
this.logError(err, 'scan', {
|
|
211
|
+
this.logError(err, 'scan', { folder: folderInfo.displayName });
|
|
150
212
|
observer.emit('error', err.message);
|
|
151
213
|
});
|
|
152
214
|
observer
|
|
@@ -154,8 +216,8 @@ let RouteLibraryScannerService = (() => {
|
|
|
154
216
|
this.importProps.scanProgress = progress;
|
|
155
217
|
})
|
|
156
218
|
.on('scan-complete', () => {
|
|
157
|
-
const cntScanned = this.importProps.scanProgress
|
|
158
|
-
this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned });
|
|
219
|
+
const { scannedFolders: cntScanned, failedFolders: cntFailed } = this.importProps.scanProgress;
|
|
220
|
+
this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned, cntFailed });
|
|
159
221
|
this.importProps.phase = 'parsing';
|
|
160
222
|
});
|
|
161
223
|
return observer;
|
|
@@ -228,189 +290,307 @@ let RouteLibraryScannerService = (() => {
|
|
|
228
290
|
this.done();
|
|
229
291
|
this.prepare();
|
|
230
292
|
}
|
|
231
|
-
async importRoute(fileInfo, observer) {
|
|
293
|
+
async importRoute(fileInfo, observer, session) {
|
|
232
294
|
await (0, sleep_1.sleep)(0);
|
|
233
|
-
|
|
234
|
-
if (
|
|
295
|
+
const isStale = () => this.importProps !== session;
|
|
296
|
+
if (isStale())
|
|
235
297
|
return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
298
|
+
session.phase = 'parsing';
|
|
299
|
+
observer.emit('parsing');
|
|
300
|
+
const isCancelled = () => this.isCancelled || isStale();
|
|
301
|
+
try {
|
|
302
|
+
const route = await this.importRouteFile(fileInfo, { isCancelled });
|
|
303
|
+
if (!isCancelled())
|
|
304
|
+
observer.emit('success', route);
|
|
239
305
|
}
|
|
240
|
-
|
|
241
|
-
|
|
306
|
+
catch (err) {
|
|
307
|
+
const error = (0, errors_1.toRouteImportError)(err);
|
|
308
|
+
if (!isCancelled())
|
|
309
|
+
observer.emit('error', error.message, error.toFailure());
|
|
242
310
|
}
|
|
243
311
|
}
|
|
244
|
-
async
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
312
|
+
async _importFiles(files, observer) {
|
|
313
|
+
await (0, utils_1.waitNextTick)();
|
|
314
|
+
const targets = files.filter(f => !!f);
|
|
315
|
+
const total = targets.length;
|
|
316
|
+
const failedRoutes = [];
|
|
317
|
+
const importedRoutes = [];
|
|
318
|
+
const isCancelled = () => this.isCancelled;
|
|
319
|
+
for (let i = 0; i < total; i++) {
|
|
320
|
+
if (isCancelled())
|
|
321
|
+
continue;
|
|
322
|
+
const file = targets[i];
|
|
323
|
+
const name = file.name ?? file.base ?? file.filename ?? file.url;
|
|
324
|
+
observer.emit('ingest-progress', { current: i + 1, total, currentName: name });
|
|
325
|
+
try {
|
|
326
|
+
importedRoutes.push(await this.importRouteFile(file, { isCancelled }));
|
|
256
327
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
cardInfo?.card?.verify();
|
|
262
|
-
if (existing) {
|
|
263
|
-
existing.card.emitUpdate();
|
|
328
|
+
catch (err) {
|
|
329
|
+
const error = (0, errors_1.toRouteImportError)(err);
|
|
330
|
+
failedRoutes.push({ name, reason: error.message, code: error.code, missingExt: error.missingExt });
|
|
331
|
+
observer.emit('ingest-error', { name, reason: error.message });
|
|
264
332
|
}
|
|
265
|
-
observer.emit('success', route);
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
|
|
269
|
-
observer.emit('error', err.message);
|
|
270
333
|
}
|
|
334
|
+
observer.emit('ingest-complete', { imported: importedRoutes.length, skipped: 0, errors: failedRoutes.length, failedRoutes, importedRoutes });
|
|
271
335
|
}
|
|
272
|
-
async
|
|
273
|
-
const
|
|
274
|
-
|
|
336
|
+
async parseRouteFile(fileInfo, isCancelled) {
|
|
337
|
+
const ext = (fileInfo?.ext ?? '').replace(/^\./, '').toLowerCase();
|
|
338
|
+
if (ext === 'gpx' || !fileInfo?.dir) {
|
|
339
|
+
const { data, details } = await parsers_1.RouteParser.parse(fileInfo);
|
|
340
|
+
return new route_1.Route(data, details);
|
|
341
|
+
}
|
|
275
342
|
const parsers = this.getParsers();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
343
|
+
if (!parsers.isPrimaryExtension(ext))
|
|
344
|
+
throw new errors_1.RouteImportError('not a route control file', 'UNSUPPORTED');
|
|
345
|
+
const target = await this.locateRouteFile(fileInfo, ext, parsers);
|
|
346
|
+
return this.parseLocatedRoute(target, isCancelled);
|
|
347
|
+
}
|
|
348
|
+
async locateRouteFile(fileInfo, ext, parsers) {
|
|
349
|
+
const { dir } = fileInfo;
|
|
350
|
+
const delimiter = fileInfo.delimiter ?? '/';
|
|
351
|
+
const folderUri = dir.endsWith(delimiter) ? dir.slice(0, -delimiter.length) : dir;
|
|
352
|
+
let listing = null;
|
|
281
353
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
354
|
+
listing = await this.listEntries(folderUri);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
this.logError(err, 'locateRouteFile');
|
|
358
|
+
}
|
|
359
|
+
const files = (listing ?? [])
|
|
360
|
+
.filter(e => !e.isDirectory)
|
|
361
|
+
.map(e => e.unknownType ? { name: e.name, uri: e.uri, isDirectory: false } : e);
|
|
362
|
+
const base = fileInfo.base ?? fileInfo.name;
|
|
363
|
+
const controlFile = base ? (files.find(f => f.name === base) ?? files.find(f => f.uri?.includes(base))) : undefined;
|
|
364
|
+
if (!controlFile)
|
|
365
|
+
throw new errors_1.RouteImportError('no file found', 'READ_FAILED');
|
|
366
|
+
const baseName = this.getBaseName(controlFile.name);
|
|
367
|
+
const missingExt = this.findMissingCompanion(parsers, ext, baseName, files);
|
|
368
|
+
if (missingExt)
|
|
369
|
+
throw new errors_1.RouteImportError(`Missing companion file (.${missingExt})`, 'MISSING_COMPANION', { missingExt });
|
|
370
|
+
return {
|
|
371
|
+
folderUri,
|
|
372
|
+
folderName: folderUri,
|
|
373
|
+
files,
|
|
374
|
+
controlFileUri: controlFile.uri,
|
|
375
|
+
format: this.getExtension(controlFile.name)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async parseLocatedRoute(target, isCancelled) {
|
|
379
|
+
const file = this.buildFileInfo(target.controlFileUri, target.format);
|
|
380
|
+
const scope = (0, externalFiles_1.useExternalFileService)().beginScope({ isCancelled });
|
|
381
|
+
try {
|
|
382
|
+
let result;
|
|
383
|
+
try {
|
|
384
|
+
result = await parsers_1.RouteParser.parse(file);
|
|
300
385
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const parseObserver = this.parse([file]);
|
|
304
|
-
parseObserver.on('parse-result', (result) => {
|
|
305
|
-
parseObserver.stop();
|
|
306
|
-
if (result.parseError) {
|
|
307
|
-
this.logEvent({ message: 'import single route file failed', file: name, reason: result.parseError });
|
|
308
|
-
observer.emit('error', result.parseError);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
const ingest = this.ingest([result]);
|
|
312
|
-
let ingestError;
|
|
313
|
-
ingest.once('ingest-error', (_, reason) => {
|
|
314
|
-
ingest.stop();
|
|
315
|
-
this.logEvent({ message: 'import single route file failed', file: name, reason });
|
|
316
|
-
observer.emit('error', reason);
|
|
317
|
-
ingestError = reason;
|
|
318
|
-
});
|
|
319
|
-
ingest.once('ingest-complete', (summary) => {
|
|
320
|
-
ingest.stop();
|
|
321
|
-
if (!ingestError && summary.imported > 0) {
|
|
322
|
-
this.logEvent({ message: 'import single route file success', file: name });
|
|
323
|
-
observer.emit('success', summary.importedRoutes?.[0]?.title);
|
|
324
|
-
}
|
|
325
|
-
else if (!ingestError && summary.imported === 0) {
|
|
326
|
-
this.logEvent({ message: 'import single route file failed', file: name, reason: 'not imported' });
|
|
327
|
-
observer.emit('error', 'not imported');
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
});
|
|
386
|
+
finally {
|
|
387
|
+
scope.end();
|
|
331
388
|
}
|
|
389
|
+
if (scope.lastFailure)
|
|
390
|
+
throw new Error(this.getReadFailureMessage(scope));
|
|
391
|
+
const route = new route_1.Route(result.data, result.details);
|
|
392
|
+
if (result.data.hasVideo)
|
|
393
|
+
this.validateVideoUrl(route, target.folderUri, target.files);
|
|
394
|
+
return route;
|
|
332
395
|
}
|
|
333
396
|
catch (err) {
|
|
334
|
-
|
|
335
|
-
observer.emit('error', err.message);
|
|
397
|
+
throw (0, errors_1.toRouteImportError)(err, scope, scope.lastFailure ? this.getReadFailureMessage(scope) : undefined);
|
|
336
398
|
}
|
|
337
399
|
}
|
|
338
400
|
async _scan(folderInfo, observer) {
|
|
339
401
|
await (0, utils_1.waitNextTick)();
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.logEvent({ message: 'video scan result', scannedFolders: this.importProps.scanProgress.scannedFolders, files: this.scanResult.length });
|
|
402
|
+
const ctx = this.createScanContext(observer, this.getParsers(), true);
|
|
403
|
+
await this.scanFolder(folderInfo.uri, folderInfo.displayName, ctx);
|
|
404
|
+
await this.upsertImportHistory(folderInfo, ctx.discoveredCount.value);
|
|
405
|
+
const { scannedFolders, failedFolders } = ctx.progress;
|
|
406
|
+
this.logEvent({ message: 'video scan result', scannedFolders, failedFolders, files: this.scanResult.length });
|
|
346
407
|
observer.emit('scan-complete', this.scanResult);
|
|
347
408
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
409
|
+
createScanContext(observer, parsers, recursive) {
|
|
410
|
+
return {
|
|
411
|
+
observer,
|
|
412
|
+
parsers,
|
|
413
|
+
recursive,
|
|
414
|
+
progress: { scannedFolders: 0, failedFolders: 0 },
|
|
415
|
+
discoveredCount: { value: 0 }
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async scanFolder(uri, folderName, ctx) {
|
|
419
|
+
const listing = await this.readFolder(uri, folderName, ctx);
|
|
420
|
+
if (!listing)
|
|
356
421
|
return;
|
|
422
|
+
let entries = listing;
|
|
423
|
+
if (ctx.recursive && this.isNamesOnly(listing))
|
|
424
|
+
entries = await this.listRecursively(uri) ?? listing;
|
|
425
|
+
await this.scanListing(uri, folderName, entries, ctx);
|
|
426
|
+
}
|
|
427
|
+
async scanListing(uri, folderName, entries, ctx) {
|
|
428
|
+
this.logICloudPlaceholders(uri, entries);
|
|
429
|
+
ctx.progress.scannedFolders++;
|
|
430
|
+
this.emitScanProgress(ctx);
|
|
431
|
+
const { files, dirs } = await this.splitEntries(entries, ctx);
|
|
432
|
+
await this.announceRoutes(files, uri, folderName, ctx);
|
|
433
|
+
for (const dir of dirs) {
|
|
434
|
+
if (this.isCancelled || !ctx.recursive)
|
|
435
|
+
continue;
|
|
436
|
+
if (dir.listing)
|
|
437
|
+
await this.scanListing(dir.uri, dir.name, dir.listing, ctx);
|
|
438
|
+
else
|
|
439
|
+
await this.scanFolder(dir.uri, dir.name, ctx);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async announceRoutes(files, uri, folderName, ctx) {
|
|
443
|
+
const primaryFiles = files.filter(f => {
|
|
444
|
+
const ext = this.getExtension(f.name);
|
|
445
|
+
return ext && ext !== 'gpx' && ctx.parsers.isPrimaryExtension(ext);
|
|
446
|
+
});
|
|
447
|
+
for (const file of primaryFiles) {
|
|
448
|
+
if (!this.isCancelled) {
|
|
449
|
+
const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, ctx.parsers);
|
|
450
|
+
ctx.discoveredCount.value++;
|
|
451
|
+
ctx.observer.emit('scan-result', routeAnnouncement);
|
|
452
|
+
this.scanResult.push(routeAnnouncement);
|
|
453
|
+
}
|
|
357
454
|
}
|
|
455
|
+
}
|
|
456
|
+
logICloudPlaceholders(uri, entries) {
|
|
358
457
|
const iCloudPlaceholders = entries.filter(e => /^\.(.+)\.icloud$/.test(e.name));
|
|
359
458
|
if (iCloudPlaceholders.length > 0) {
|
|
360
|
-
const firstName = iCloudPlaceholders[0].name;
|
|
361
459
|
this.logEvent({
|
|
362
460
|
message: 'iCloud placeholder files detected in folder',
|
|
363
461
|
uri,
|
|
364
462
|
count: iCloudPlaceholders.length,
|
|
365
|
-
firstPlaceholder:
|
|
463
|
+
firstPlaceholder: iCloudPlaceholders[0].name
|
|
366
464
|
});
|
|
367
465
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return ext && ext !== 'gpx' && parsers.isPrimaryExtension(ext);
|
|
376
|
-
}).map((f) => {
|
|
377
|
-
if (typeof f === 'string') {
|
|
378
|
-
return {
|
|
379
|
-
name: f,
|
|
380
|
-
isDirectory: false,
|
|
381
|
-
uri: this.getBindings().path.join(uri, f)
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
else
|
|
385
|
-
return f;
|
|
386
|
-
});
|
|
387
|
-
for (const file of primaryFiles) {
|
|
388
|
-
if (!this.isCancelled) {
|
|
389
|
-
const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
|
|
390
|
-
discoveredCount.value++;
|
|
391
|
-
observer.emit('scan-result', routeAnnouncement);
|
|
392
|
-
this.scanResult.push(routeAnnouncement);
|
|
393
|
-
}
|
|
466
|
+
}
|
|
467
|
+
async readFolder(uri, folderName, ctx) {
|
|
468
|
+
try {
|
|
469
|
+
const entries = await this.listEntries(uri);
|
|
470
|
+
if (entries)
|
|
471
|
+
return entries;
|
|
472
|
+
this.countFailedFolder(ctx, new Error('folder listing not available'), folderName);
|
|
394
473
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
|
|
398
|
-
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
this.countFailedFolder(ctx, err, folderName);
|
|
399
476
|
}
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
countFailedFolder(ctx, err, folderName) {
|
|
480
|
+
ctx.progress.failedFolders++;
|
|
481
|
+
this.logError(err, 'scanFolder', { folderName });
|
|
482
|
+
this.emitScanProgress(ctx);
|
|
483
|
+
}
|
|
484
|
+
emitScanProgress(ctx) {
|
|
485
|
+
const { scannedFolders, failedFolders } = ctx.progress;
|
|
486
|
+
ctx.observer.emit('scan-progress', { scannedFolders, failedFolders });
|
|
487
|
+
}
|
|
488
|
+
async listEntries(uri) {
|
|
489
|
+
const result = await this.getBindings().fs.readdir?.(uri, { recursive: false, extended: true });
|
|
490
|
+
if (!Array.isArray(result))
|
|
491
|
+
return null;
|
|
492
|
+
if (result.length === 0 || typeof result[0] !== 'string')
|
|
493
|
+
return result;
|
|
494
|
+
const path = this.getBindings().path;
|
|
495
|
+
return result.map(name => ({ name, uri: path.join(uri, name), unknownType: true }));
|
|
496
|
+
}
|
|
497
|
+
isNamesOnly(entries) {
|
|
498
|
+
return entries.some(e => e.unknownType);
|
|
499
|
+
}
|
|
500
|
+
async splitEntries(entries, ctx) {
|
|
501
|
+
if (!this.isNamesOnly(entries)) {
|
|
502
|
+
return {
|
|
503
|
+
files: entries.filter(e => !e.isDirectory),
|
|
504
|
+
dirs: entries.filter(e => e.isDirectory)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return this.resolveNamesOnly(entries, ctx);
|
|
508
|
+
}
|
|
509
|
+
async resolveNamesOnly(entries, ctx) {
|
|
510
|
+
const files = [];
|
|
511
|
+
const dirs = [];
|
|
512
|
+
const knownFileExts = this.getKnownFileExtensions(entries, ctx.parsers);
|
|
513
|
+
for (const entry of entries) {
|
|
514
|
+
const needsProbe = ctx.recursive && !this.isCancelled && !knownFileExts.has(this.getExtension(entry.name));
|
|
515
|
+
const listing = needsProbe ? await this.probeEntry(entry, ctx) : undefined;
|
|
516
|
+
if (listing)
|
|
517
|
+
dirs.push({ name: entry.name, uri: entry.uri, isDirectory: true, listing });
|
|
518
|
+
else if (listing === undefined)
|
|
519
|
+
files.push({ name: entry.name, uri: entry.uri, isDirectory: false });
|
|
520
|
+
}
|
|
521
|
+
return { files, dirs };
|
|
522
|
+
}
|
|
523
|
+
async probeEntry(entry, ctx) {
|
|
524
|
+
try {
|
|
525
|
+
return (await this.listEntries(entry.uri)) ?? undefined;
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
if (!this.isUnreadableFolderError(err))
|
|
529
|
+
return undefined;
|
|
530
|
+
this.countFailedFolder(ctx, err, entry.name);
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
isUnreadableFolderError(err) {
|
|
535
|
+
const { code, message } = (err ?? {});
|
|
536
|
+
const text = [code, message].filter(v => typeof v === 'string').join(' ');
|
|
537
|
+
return UNREADABLE_FOLDER_ERRORS.test(text);
|
|
538
|
+
}
|
|
539
|
+
getKnownFileExtensions(entries, parsers) {
|
|
540
|
+
const known = new Set(VIDEO_EXTENSIONS);
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
const ext = this.getExtension(entry.name);
|
|
543
|
+
if (!ext || !parsers.isPrimaryExtension(ext))
|
|
544
|
+
continue;
|
|
545
|
+
known.add(ext);
|
|
546
|
+
for (const companion of this.getCompanionExts(parsers, ext))
|
|
547
|
+
known.add(companion.toLowerCase());
|
|
548
|
+
}
|
|
549
|
+
return known;
|
|
550
|
+
}
|
|
551
|
+
async listRecursively(uri) {
|
|
552
|
+
let result;
|
|
553
|
+
try {
|
|
554
|
+
result = await this.getBindings().fs.readdir?.(uri, { recursive: true });
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
if (!Array.isArray(result) || !result.every(p => typeof p === 'string'))
|
|
560
|
+
return undefined;
|
|
561
|
+
const paths = result;
|
|
562
|
+
if (!paths.some(p => PATH_SEPARATOR.test(p)))
|
|
563
|
+
return undefined;
|
|
564
|
+
return this.buildListingTree(uri, paths);
|
|
565
|
+
}
|
|
566
|
+
buildListingTree(rootUri, paths) {
|
|
567
|
+
const normalised = paths.map(p => p.split(PATH_SEPARATOR).join('/'));
|
|
568
|
+
const listed = new Set(normalised);
|
|
569
|
+
const children = new Map();
|
|
570
|
+
for (const relPath of normalised) {
|
|
571
|
+
const cut = relPath.lastIndexOf('/');
|
|
572
|
+
const parent = cut < 0 ? '' : relPath.slice(0, cut);
|
|
573
|
+
if (parent && !listed.has(parent))
|
|
574
|
+
return undefined;
|
|
575
|
+
const siblings = children.get(parent) ?? [];
|
|
576
|
+
siblings.push(relPath.slice(cut + 1));
|
|
577
|
+
children.set(parent, siblings);
|
|
578
|
+
}
|
|
579
|
+
const path = this.getBindings().path;
|
|
580
|
+
const toListing = (relDir, dirUri) => (children.get(relDir) ?? []).map(name => {
|
|
581
|
+
const relPath = relDir ? `${relDir}/${name}` : name;
|
|
582
|
+
const entryUri = path.join(dirUri, name);
|
|
583
|
+
return children.has(relPath)
|
|
584
|
+
? { name, uri: entryUri, isDirectory: true, listing: toListing(relPath, entryUri) }
|
|
585
|
+
: { name, uri: entryUri, isDirectory: false };
|
|
586
|
+
});
|
|
587
|
+
return toListing('', rootUri);
|
|
400
588
|
}
|
|
401
589
|
async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
|
|
402
590
|
const ext = this.getExtension(controlFile.name);
|
|
403
|
-
const baseName =
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
for (const compExt of companionExts) {
|
|
407
|
-
const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
408
|
-
f.name.toLowerCase().startsWith(baseName.toLowerCase()));
|
|
409
|
-
if (!hasCompanion) {
|
|
410
|
-
skipReason = `Missing companion file (.${compExt})`;
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
591
|
+
const baseName = this.getBaseName(controlFile.name);
|
|
592
|
+
const missingExt = this.findMissingCompanion(parsers, ext, baseName, folderFiles);
|
|
593
|
+
const skipReason = missingExt ? `Missing companion file (.${missingExt})` : undefined;
|
|
414
594
|
return {
|
|
415
595
|
folderUri,
|
|
416
596
|
folderName,
|
|
@@ -431,6 +611,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
431
611
|
parseState: 'waiting',
|
|
432
612
|
importable: false,
|
|
433
613
|
label: file.base,
|
|
614
|
+
folder: target.folderName,
|
|
434
615
|
id: target.controlFileUri,
|
|
435
616
|
alreadyImported: false,
|
|
436
617
|
observer: new types_1.Observer()
|
|
@@ -489,6 +670,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
489
670
|
alreadyImported: false,
|
|
490
671
|
route,
|
|
491
672
|
folderUri: target.folderUri,
|
|
673
|
+
folderName: target.folderName,
|
|
492
674
|
controlFileUri: target.controlFileUri,
|
|
493
675
|
format: target.format
|
|
494
676
|
};
|
|
@@ -503,36 +685,16 @@ let RouteLibraryScannerService = (() => {
|
|
|
503
685
|
alreadyImported: false,
|
|
504
686
|
route: result ? new route_1.Route(result.data, result.details) : undefined,
|
|
505
687
|
folderUri: target.folderUri,
|
|
688
|
+
folderName: target.folderName,
|
|
506
689
|
controlFileUri: target.controlFileUri,
|
|
507
690
|
format: target.format,
|
|
508
691
|
parseError: scope.lastFailure ? this.getReadFailureMessage(scope) : (err?.message ?? String(err)),
|
|
509
|
-
parseErrorCode:
|
|
692
|
+
parseErrorCode: (0, errors_1.mapErrorToImportCode)(err, scope)
|
|
510
693
|
};
|
|
511
694
|
this.logEvent({ message: 'could not parse route file', file: file.base, reason: err.message, stack: err.stack });
|
|
512
695
|
observer.emit('parse-result', parsed);
|
|
513
696
|
}
|
|
514
697
|
}
|
|
515
|
-
mapErrorToImportCode(err, scope) {
|
|
516
|
-
switch (scope?.lastFailure?.reason) {
|
|
517
|
-
case 'offline':
|
|
518
|
-
return 'ICLOUD_OFFLINE';
|
|
519
|
-
case 'timeout':
|
|
520
|
-
case 'download-failed':
|
|
521
|
-
return 'ICLOUD_DOWNLOAD_FAILED';
|
|
522
|
-
case 'access-lost':
|
|
523
|
-
return 'READ_FAILED';
|
|
524
|
-
}
|
|
525
|
-
const message = err?.message ?? String(err);
|
|
526
|
-
if (/AVI/i.test(message))
|
|
527
|
-
return 'AVI_NOT_SUPPORTED';
|
|
528
|
-
if (/no video/i.test(message))
|
|
529
|
-
return 'NO_VIDEO';
|
|
530
|
-
if (/^Could not (open|read)/i.test(message))
|
|
531
|
-
return 'READ_FAILED';
|
|
532
|
-
if (/pars(e|ing)/i.test(message))
|
|
533
|
-
return 'PARSE_FAILED';
|
|
534
|
-
return 'UNSUPPORTED';
|
|
535
|
-
}
|
|
536
698
|
getReadFailureMessage(scope) {
|
|
537
699
|
const name = this.getFileName(scope.failedFile);
|
|
538
700
|
switch (scope.lastFailure?.reason) {
|
|
@@ -560,6 +722,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
560
722
|
validateVideoUrl(route, folderUri, folderFiles) {
|
|
561
723
|
const { video } = route?.details ?? {};
|
|
562
724
|
const { file, url, format } = video ?? {};
|
|
725
|
+
this.logEvent({ message: 'validateVideoUrl', file: this.getFileName(file), url: this.getFileName(url), format, folderFileCount: folderFiles.length });
|
|
563
726
|
let videoFormat = format;
|
|
564
727
|
try {
|
|
565
728
|
if (videoFormat === undefined) {
|
|
@@ -642,7 +805,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
642
805
|
catch (err) {
|
|
643
806
|
const reason = err?.message ?? String(err);
|
|
644
807
|
errors++;
|
|
645
|
-
failedRoutes.push({ name: route.title, reason, code:
|
|
808
|
+
failedRoutes.push({ name: route.title, reason, code: (0, errors_1.mapErrorToImportCode)(err) });
|
|
646
809
|
observer.emit('ingest-error', { name: route.title, reason });
|
|
647
810
|
}
|
|
648
811
|
}
|
|
@@ -660,6 +823,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
660
823
|
catch (err) {
|
|
661
824
|
this.logError(err, 'adoptPreview', { title: route?.title });
|
|
662
825
|
}
|
|
826
|
+
await this.getRouteShapeStore().saveOnImport(route);
|
|
663
827
|
await db.save(route, true);
|
|
664
828
|
service.addRoute(route, existing ? 'import' : 'user');
|
|
665
829
|
try {
|
|
@@ -679,6 +843,9 @@ let RouteLibraryScannerService = (() => {
|
|
|
679
843
|
if (videoRef.startsWith('content://')) {
|
|
680
844
|
return videoRef;
|
|
681
845
|
}
|
|
846
|
+
if (!this.isMobile() && /^[a-z][a-z0-9+.-]*:\/\//i.test(videoRef)) {
|
|
847
|
+
return videoRef;
|
|
848
|
+
}
|
|
682
849
|
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
683
850
|
if (this.isMobile())
|
|
684
851
|
throw new Error('Absolute video path references are not supported');
|
|
@@ -691,24 +858,44 @@ let RouteLibraryScannerService = (() => {
|
|
|
691
858
|
}
|
|
692
859
|
async upsertImportHistory(folderInfo, routeCount) {
|
|
693
860
|
try {
|
|
861
|
+
const treeUri = this.normaliseTreeUri(folderInfo.uri);
|
|
694
862
|
const repo = json_1.JsonRepository.create('importedLibraries');
|
|
695
|
-
const names = await repo.list();
|
|
696
|
-
const all = await Promise.all(
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
863
|
+
const names = (await repo.list()) ?? [];
|
|
864
|
+
const all = await Promise.all(names.map(n => repo.read(n)));
|
|
865
|
+
const matching = names.filter((_, i) => {
|
|
866
|
+
const lib = all[i];
|
|
867
|
+
return typeof lib?.treeUri === 'string' && this.normaliseTreeUri(lib.treeUri) === treeUri;
|
|
868
|
+
});
|
|
869
|
+
const [kept, ...duplicates] = matching;
|
|
870
|
+
const existing = (kept === undefined ? undefined : all[names.indexOf(kept)]);
|
|
871
|
+
for (const duplicate of duplicates)
|
|
872
|
+
await repo.delete(duplicate);
|
|
700
873
|
const id = existing?.id ?? (0, uuid_1.v4)();
|
|
701
874
|
await repo.write(id, {
|
|
702
875
|
id,
|
|
703
|
-
treeUri
|
|
876
|
+
treeUri,
|
|
704
877
|
displayName: folderInfo.displayName,
|
|
705
878
|
lastScanned: new Date().toISOString(),
|
|
706
879
|
routeCount
|
|
707
880
|
});
|
|
708
881
|
}
|
|
709
882
|
catch (err) {
|
|
710
|
-
this.logError(err, 'upsertImportHistory', {
|
|
711
|
-
}
|
|
883
|
+
this.logError(err, 'upsertImportHistory', { folder: folderInfo.displayName });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
normaliseTreeUri(uri) {
|
|
887
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(uri))
|
|
888
|
+
return uri;
|
|
889
|
+
let end = uri.length;
|
|
890
|
+
while (end > 0 && (uri[end - 1] === '/' || uri[end - 1] === '\\'))
|
|
891
|
+
end--;
|
|
892
|
+
const trimmed = uri.slice(0, end);
|
|
893
|
+
const withDrive = trimmed.replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
894
|
+
if (withDrive === '')
|
|
895
|
+
return uri.slice(0, 1);
|
|
896
|
+
if (/^[A-Z]:$/.test(withDrive))
|
|
897
|
+
return withDrive + uri.charAt(2);
|
|
898
|
+
return withDrive;
|
|
712
899
|
}
|
|
713
900
|
buildFileInfo(uri, ext) {
|
|
714
901
|
const { dir, base, name } = this.getBindings().path.parse(uri);
|
|
@@ -727,7 +914,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
727
914
|
return this.importProps.routes.find(r => r.id === parsed.controlFileUri);
|
|
728
915
|
}
|
|
729
916
|
buildRouteDisplayItem(parsed, observer) {
|
|
730
|
-
const { route, alreadyImported, parseError, format, controlFileUri } = parsed;
|
|
917
|
+
const { route, alreadyImported, parseError, format, controlFileUri, folderName } = parsed;
|
|
731
918
|
const descr = route?.description ?? {};
|
|
732
919
|
const [C, U] = this.getUnitConversionShortcuts();
|
|
733
920
|
const distance = descr.distance === undefined ? undefined : {
|
|
@@ -741,6 +928,7 @@ let RouteLibraryScannerService = (() => {
|
|
|
741
928
|
id: route?.description?.id ?? info?.base,
|
|
742
929
|
distance,
|
|
743
930
|
label: route?.title ?? info?.base,
|
|
931
|
+
folder: folderName,
|
|
744
932
|
alreadyImported,
|
|
745
933
|
parseState: importProps?.parseState ?? 'waiting',
|
|
746
934
|
importable: parseError == null,
|
|
@@ -760,6 +948,14 @@ let RouteLibraryScannerService = (() => {
|
|
|
760
948
|
return [];
|
|
761
949
|
}
|
|
762
950
|
}
|
|
951
|
+
findMissingCompanion(parsers, primaryExt, baseName, folderFiles) {
|
|
952
|
+
return this.getCompanionExts(parsers, primaryExt).find(compExt => !folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
953
|
+
f.name.toLowerCase().startsWith(baseName.toLowerCase())));
|
|
954
|
+
}
|
|
955
|
+
getBaseName(filename) {
|
|
956
|
+
const ext = this.getExtension(filename);
|
|
957
|
+
return filename.slice(0, filename.length - ext.length - 1);
|
|
958
|
+
}
|
|
763
959
|
getExtension(filename) {
|
|
764
960
|
const dot = filename.lastIndexOf('.');
|
|
765
961
|
return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
@@ -779,6 +975,9 @@ let RouteLibraryScannerService = (() => {
|
|
|
779
975
|
getPreviewStore() {
|
|
780
976
|
return (0, store_1.usePreviewStore)();
|
|
781
977
|
}
|
|
978
|
+
getRouteShapeStore() {
|
|
979
|
+
return (0, store_2.useRouteShapeStore)();
|
|
980
|
+
}
|
|
782
981
|
getParsers() {
|
|
783
982
|
return (0, parsers_1.useParsers)();
|
|
784
983
|
}
|