incyclist-services 1.7.51 → 1.7.52
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/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 +327 -0
- package/lib/cjs/routes/library/types.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 +323 -0
- package/lib/esm/routes/library/types.js +1 -0
- 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 +0 -11
- package/lib/types/routes/library/service.d.ts +28 -0
- package/lib/types/routes/library/types.d.ts +57 -0
- package/lib/types/routes/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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,327 @@
|
|
|
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 VIDEO_EXTENSIONS = new Set(['mp4', 'mov', 'mkv', 'm4v', 'mpg', 'mpeg', 'wmv', 'avi']);
|
|
48
|
+
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
|
|
49
|
+
let RouteLibraryScannerService = (() => {
|
|
50
|
+
let _classDecorators = [decorators_1.Singleton];
|
|
51
|
+
let _classDescriptor;
|
|
52
|
+
let _classExtraInitializers = [];
|
|
53
|
+
let _classThis;
|
|
54
|
+
let _classSuper = service_1.IncyclistService;
|
|
55
|
+
let _instanceExtraInitializers = [];
|
|
56
|
+
let _getRouteList_decorators;
|
|
57
|
+
let _getBindings_decorators;
|
|
58
|
+
var RouteLibraryScannerService = class extends _classSuper {
|
|
59
|
+
static { _classThis = this; }
|
|
60
|
+
static {
|
|
61
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
62
|
+
_getRouteList_decorators = [decorators_1.Injectable];
|
|
63
|
+
_getBindings_decorators = [decorators_1.Injectable];
|
|
64
|
+
__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
|
+
__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);
|
|
66
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
67
|
+
RouteLibraryScannerService = _classThis = _classDescriptor.value;
|
|
68
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
69
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
70
|
+
}
|
|
71
|
+
constructor() {
|
|
72
|
+
super('RouteLibraryScanner');
|
|
73
|
+
__runInitializers(this, _instanceExtraInitializers);
|
|
74
|
+
}
|
|
75
|
+
scan(folderInfo) {
|
|
76
|
+
const observer = new types_1.Observer();
|
|
77
|
+
this._scan(folderInfo, observer).catch(err => {
|
|
78
|
+
this.logError(err, 'scan', { uri: folderInfo.uri });
|
|
79
|
+
observer.emit('error', err.message);
|
|
80
|
+
});
|
|
81
|
+
return observer;
|
|
82
|
+
}
|
|
83
|
+
ingest(routes, treeUri) {
|
|
84
|
+
const observer = new types_1.Observer();
|
|
85
|
+
this._ingest(routes, treeUri, observer).catch(err => {
|
|
86
|
+
this.logError(err, 'ingest', { treeUri });
|
|
87
|
+
observer.emit('error', err.message);
|
|
88
|
+
});
|
|
89
|
+
return observer;
|
|
90
|
+
}
|
|
91
|
+
async _scan(folderInfo, observer) {
|
|
92
|
+
await (0, utils_1.waitNextTick)();
|
|
93
|
+
const parsers = (0, parsers_1.useParsers)();
|
|
94
|
+
const progress = { scannedFolders: 0 };
|
|
95
|
+
const discoveredCount = { value: 0 };
|
|
96
|
+
await this.scanFolder(folderInfo.uri, folderInfo.displayName, observer, parsers, progress, discoveredCount);
|
|
97
|
+
await this.upsertImportHistory(folderInfo, discoveredCount.value);
|
|
98
|
+
observer.emit('scan-complete');
|
|
99
|
+
}
|
|
100
|
+
async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount) {
|
|
101
|
+
const fs = this.getBindings().fs;
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = await fs.readdir(uri, { extended: true });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
this.logError(err, 'scanFolder', { uri });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
progress.scannedFolders++;
|
|
111
|
+
observer.emit('scan-progress', { scannedFolders: progress.scannedFolders });
|
|
112
|
+
const files = entries.filter(e => !e.isDirectory);
|
|
113
|
+
const dirs = entries.filter(e => e.isDirectory);
|
|
114
|
+
const primaryFiles = files.filter(f => {
|
|
115
|
+
const ext = this.getExtension(f.name);
|
|
116
|
+
return ext && parsers.isPrimaryExtension(ext);
|
|
117
|
+
});
|
|
118
|
+
for (const file of primaryFiles) {
|
|
119
|
+
const route = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
|
|
120
|
+
discoveredCount.value++;
|
|
121
|
+
observer.emit('discovered', route);
|
|
122
|
+
}
|
|
123
|
+
for (const dir of dirs) {
|
|
124
|
+
await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
|
|
128
|
+
const ext = this.getExtension(controlFile.name);
|
|
129
|
+
const baseName = controlFile.name.slice(0, controlFile.name.length - ext.length - 1);
|
|
130
|
+
let importable = true;
|
|
131
|
+
let skipReason;
|
|
132
|
+
const companionExts = this.getCompanionExts(parsers, ext);
|
|
133
|
+
for (const compExt of companionExts) {
|
|
134
|
+
const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
135
|
+
f.name.toLowerCase().startsWith(baseName.toLowerCase()));
|
|
136
|
+
if (!hasCompanion) {
|
|
137
|
+
importable = false;
|
|
138
|
+
skipReason = `Missing companion file (.${compExt})`;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
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
|
+
return {
|
|
176
|
+
id: (0, uuid_1.v4)(),
|
|
177
|
+
folderUri,
|
|
178
|
+
folderName,
|
|
179
|
+
controlFileUri: controlFile.uri,
|
|
180
|
+
format: ext,
|
|
181
|
+
hasVideo,
|
|
182
|
+
hasThumbnail,
|
|
183
|
+
alreadyImported,
|
|
184
|
+
importable,
|
|
185
|
+
skipReason
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async _ingest(routes, treeUri, observer) {
|
|
189
|
+
await (0, utils_1.waitNextTick)();
|
|
190
|
+
const importable = routes.filter(r => r.importable && !r.alreadyImported);
|
|
191
|
+
const total = importable.length;
|
|
192
|
+
let imported = 0;
|
|
193
|
+
let errors = 0;
|
|
194
|
+
const failedRoutes = [];
|
|
195
|
+
for (let i = 0; i < importable.length; i++) {
|
|
196
|
+
const route = importable[i];
|
|
197
|
+
observer.emit('ingest-progress', { current: i + 1, total, currentName: route.folderName });
|
|
198
|
+
try {
|
|
199
|
+
await this.ingestRoute(route, treeUri);
|
|
200
|
+
imported++;
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const reason = err?.message ?? String(err);
|
|
204
|
+
errors++;
|
|
205
|
+
failedRoutes.push({ name: route.folderName, reason });
|
|
206
|
+
observer.emit('ingest-error', { name: route.folderName, reason });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const skipped = routes.length - importable.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;
|
|
245
|
+
}
|
|
246
|
+
resolveVideoUri(videoRef, folderUri) {
|
|
247
|
+
if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
|
|
248
|
+
return videoRef;
|
|
249
|
+
}
|
|
250
|
+
if (videoRef.startsWith('content://')) {
|
|
251
|
+
return videoRef;
|
|
252
|
+
}
|
|
253
|
+
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
254
|
+
throw new Error('Absolute video path references are not supported during ingest');
|
|
255
|
+
}
|
|
256
|
+
return `${folderUri}/${videoRef}`;
|
|
257
|
+
}
|
|
258
|
+
async upsertImportHistory(folderInfo, routeCount) {
|
|
259
|
+
try {
|
|
260
|
+
const repo = json_1.JsonRepository.create('importedLibraries');
|
|
261
|
+
const names = await repo.list();
|
|
262
|
+
const all = await Promise.all((names ?? []).map(n => repo.read(n)));
|
|
263
|
+
const existing = all
|
|
264
|
+
.map(lib => lib)
|
|
265
|
+
.find(lib => lib?.treeUri === folderInfo.uri);
|
|
266
|
+
const id = existing?.id ?? (0, uuid_1.v4)();
|
|
267
|
+
await repo.write(id, {
|
|
268
|
+
id,
|
|
269
|
+
treeUri: folderInfo.uri,
|
|
270
|
+
displayName: folderInfo.displayName,
|
|
271
|
+
lastScanned: new Date().toISOString(),
|
|
272
|
+
routeCount
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
this.logError(err, 'upsertImportHistory', { uri: folderInfo.uri });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
buildFileInfo(uri, ext) {
|
|
280
|
+
const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\'));
|
|
281
|
+
const dir = uri.slice(0, lastSlash);
|
|
282
|
+
const base = uri.slice(lastSlash + 1);
|
|
283
|
+
const name = base.slice(0, base.length - ext.length - 1);
|
|
284
|
+
return {
|
|
285
|
+
type: 'url',
|
|
286
|
+
url: uri,
|
|
287
|
+
filename: uri,
|
|
288
|
+
base,
|
|
289
|
+
name,
|
|
290
|
+
dir,
|
|
291
|
+
ext,
|
|
292
|
+
delimiter: '/'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
getCompanionExts(parsers, primaryExt) {
|
|
296
|
+
try {
|
|
297
|
+
const matching = parsers.suppertsExtension(primaryExt);
|
|
298
|
+
const parser = matching.find(p => p.getPrimaryExtension() === primaryExt);
|
|
299
|
+
return parser?.getCompanionExtensions() ?? [];
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
getExtension(filename) {
|
|
306
|
+
const dot = filename.lastIndexOf('.');
|
|
307
|
+
return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
308
|
+
}
|
|
309
|
+
containsAbsolutePath(content) {
|
|
310
|
+
if (/[>"']\//m.test(content))
|
|
311
|
+
return true;
|
|
312
|
+
if (/[A-Za-z]:[/\\]/m.test(content))
|
|
313
|
+
return true;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
getRouteList() {
|
|
317
|
+
return (0, service_2.useRouteList)();
|
|
318
|
+
}
|
|
319
|
+
getBindings() {
|
|
320
|
+
return (0, api_1.getBindings)();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
return RouteLibraryScannerService = _classThis;
|
|
324
|
+
})();
|
|
325
|
+
exports.RouteLibraryScannerService = RouteLibraryScannerService;
|
|
326
|
+
const useRouteLibraryScanner = () => new RouteLibraryScannerService();
|
|
327
|
+
exports.useRouteLibraryScanner = useRouteLibraryScanner;
|
|
@@ -7,6 +7,12 @@ export class EPMParser extends XMLParser {
|
|
|
7
7
|
supportsExtension(extension) {
|
|
8
8
|
return extension.toLowerCase() === 'epm';
|
|
9
9
|
}
|
|
10
|
+
getPrimaryExtension() {
|
|
11
|
+
return 'epm';
|
|
12
|
+
}
|
|
13
|
+
getCompanionExtensions() {
|
|
14
|
+
return ['epp'];
|
|
15
|
+
}
|
|
10
16
|
async loadPoints(context) {
|
|
11
17
|
const { data, route } = context;
|
|
12
18
|
route.points = [];
|
|
@@ -21,6 +21,11 @@ export class ParserFactory {
|
|
|
21
21
|
throw new Error(`invalid file format ${extension}`);
|
|
22
22
|
return matching;
|
|
23
23
|
}
|
|
24
|
+
isPrimaryExtension(extension) {
|
|
25
|
+
const ext = extension.toLowerCase();
|
|
26
|
+
const isPrimary = this.parsers.some(p => p.getPrimaryExtension() === ext);
|
|
27
|
+
return isPrimary;
|
|
28
|
+
}
|
|
24
29
|
findMatching(extension, data) {
|
|
25
30
|
const matching = this.parsers
|
|
26
31
|
.filter(p => p.supportsExtension(extension))
|
|
@@ -49,6 +49,12 @@ let GeometryParser = (() => {
|
|
|
49
49
|
const geometry = await this.getData(file, data);
|
|
50
50
|
return await this.parse(file, geometry);
|
|
51
51
|
}
|
|
52
|
+
getPrimaryExtension() {
|
|
53
|
+
return 'xml';
|
|
54
|
+
}
|
|
55
|
+
getCompanionExtensions() {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
52
58
|
supportsExtension(extension) {
|
|
53
59
|
return extension.toLowerCase() === 'json';
|
|
54
60
|
}
|
|
@@ -11,6 +11,12 @@ export class GPXParser extends XMLParser {
|
|
|
11
11
|
super();
|
|
12
12
|
this.props = props;
|
|
13
13
|
}
|
|
14
|
+
getPrimaryExtension() {
|
|
15
|
+
return 'gpx';
|
|
16
|
+
}
|
|
17
|
+
getCompanionExtensions() {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
14
20
|
supportsExtension(extension) {
|
|
15
21
|
return extension.toLowerCase() === 'gpx';
|
|
16
22
|
}
|
|
@@ -17,6 +17,12 @@ export class MultipleXMLParser {
|
|
|
17
17
|
supportsExtension(extension) {
|
|
18
18
|
return extension?.toLowerCase() === 'xml';
|
|
19
19
|
}
|
|
20
|
+
getPrimaryExtension() {
|
|
21
|
+
return 'xml';
|
|
22
|
+
}
|
|
23
|
+
getCompanionExtensions() {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
20
26
|
supportsContent() {
|
|
21
27
|
return true;
|
|
22
28
|
}
|
|
@@ -33,6 +33,12 @@ export class TacxParser {
|
|
|
33
33
|
supportsContent(data) {
|
|
34
34
|
return TacxFileReader.isValid(data);
|
|
35
35
|
}
|
|
36
|
+
getPrimaryExtension() {
|
|
37
|
+
return 'rlv';
|
|
38
|
+
}
|
|
39
|
+
getCompanionExtensions() {
|
|
40
|
+
return ['pgmf'];
|
|
41
|
+
}
|
|
36
42
|
buildContext(file) {
|
|
37
43
|
const { dir, delimiter: d } = file;
|
|
38
44
|
if (file.ext === 'rlv') {
|
|
@@ -15,6 +15,12 @@ export class XMLParser {
|
|
|
15
15
|
const C = this.constructor;
|
|
16
16
|
return C['SCHEME'];
|
|
17
17
|
}
|
|
18
|
+
getPrimaryExtension() {
|
|
19
|
+
return 'xml';
|
|
20
|
+
}
|
|
21
|
+
getCompanionExtensions() {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
18
24
|
supportsExtension(extension) {
|
|
19
25
|
return extension.toLowerCase() === 'xml';
|
|
20
26
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
36
|
+
import { getBindings } from '../../api';
|
|
37
|
+
import { JsonRepository } from '../../api/repository/json';
|
|
38
|
+
import { Injectable, Singleton } from '../../base/decorators';
|
|
39
|
+
import { IncyclistService } from '../../base/service';
|
|
40
|
+
import { Observer } from '../../base/types';
|
|
41
|
+
import { RouteParser, useParsers } from '../base/parsers';
|
|
42
|
+
import { useRouteList } from '../list/service';
|
|
43
|
+
import { waitNextTick } from '../../utils';
|
|
44
|
+
const VIDEO_EXTENSIONS = new Set(['mp4', 'mov', 'mkv', 'm4v', 'mpg', 'mpeg', 'wmv', 'avi']);
|
|
45
|
+
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
|
|
46
|
+
let RouteLibraryScannerService = (() => {
|
|
47
|
+
let _classDecorators = [Singleton];
|
|
48
|
+
let _classDescriptor;
|
|
49
|
+
let _classExtraInitializers = [];
|
|
50
|
+
let _classThis;
|
|
51
|
+
let _classSuper = IncyclistService;
|
|
52
|
+
let _instanceExtraInitializers = [];
|
|
53
|
+
let _getRouteList_decorators;
|
|
54
|
+
let _getBindings_decorators;
|
|
55
|
+
var RouteLibraryScannerService = class extends _classSuper {
|
|
56
|
+
static { _classThis = this; }
|
|
57
|
+
static {
|
|
58
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
59
|
+
_getRouteList_decorators = [Injectable];
|
|
60
|
+
_getBindings_decorators = [Injectable];
|
|
61
|
+
__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);
|
|
62
|
+
__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);
|
|
63
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
64
|
+
RouteLibraryScannerService = _classThis = _classDescriptor.value;
|
|
65
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
66
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
67
|
+
}
|
|
68
|
+
constructor() {
|
|
69
|
+
super('RouteLibraryScanner');
|
|
70
|
+
__runInitializers(this, _instanceExtraInitializers);
|
|
71
|
+
}
|
|
72
|
+
scan(folderInfo) {
|
|
73
|
+
const observer = new Observer();
|
|
74
|
+
this._scan(folderInfo, observer).catch(err => {
|
|
75
|
+
this.logError(err, 'scan', { uri: folderInfo.uri });
|
|
76
|
+
observer.emit('error', err.message);
|
|
77
|
+
});
|
|
78
|
+
return observer;
|
|
79
|
+
}
|
|
80
|
+
ingest(routes, treeUri) {
|
|
81
|
+
const observer = new Observer();
|
|
82
|
+
this._ingest(routes, treeUri, observer).catch(err => {
|
|
83
|
+
this.logError(err, 'ingest', { treeUri });
|
|
84
|
+
observer.emit('error', err.message);
|
|
85
|
+
});
|
|
86
|
+
return observer;
|
|
87
|
+
}
|
|
88
|
+
async _scan(folderInfo, observer) {
|
|
89
|
+
await waitNextTick();
|
|
90
|
+
const parsers = useParsers();
|
|
91
|
+
const progress = { scannedFolders: 0 };
|
|
92
|
+
const discoveredCount = { value: 0 };
|
|
93
|
+
await this.scanFolder(folderInfo.uri, folderInfo.displayName, observer, parsers, progress, discoveredCount);
|
|
94
|
+
await this.upsertImportHistory(folderInfo, discoveredCount.value);
|
|
95
|
+
observer.emit('scan-complete');
|
|
96
|
+
}
|
|
97
|
+
async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount) {
|
|
98
|
+
const fs = this.getBindings().fs;
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await fs.readdir(uri, { extended: true });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.logError(err, 'scanFolder', { uri });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
progress.scannedFolders++;
|
|
108
|
+
observer.emit('scan-progress', { scannedFolders: progress.scannedFolders });
|
|
109
|
+
const files = entries.filter(e => !e.isDirectory);
|
|
110
|
+
const dirs = entries.filter(e => e.isDirectory);
|
|
111
|
+
const primaryFiles = files.filter(f => {
|
|
112
|
+
const ext = this.getExtension(f.name);
|
|
113
|
+
return ext && parsers.isPrimaryExtension(ext);
|
|
114
|
+
});
|
|
115
|
+
for (const file of primaryFiles) {
|
|
116
|
+
const route = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
|
|
117
|
+
discoveredCount.value++;
|
|
118
|
+
observer.emit('discovered', route);
|
|
119
|
+
}
|
|
120
|
+
for (const dir of dirs) {
|
|
121
|
+
await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
|
|
125
|
+
const ext = this.getExtension(controlFile.name);
|
|
126
|
+
const baseName = controlFile.name.slice(0, controlFile.name.length - ext.length - 1);
|
|
127
|
+
let importable = true;
|
|
128
|
+
let skipReason;
|
|
129
|
+
const companionExts = this.getCompanionExts(parsers, ext);
|
|
130
|
+
for (const compExt of companionExts) {
|
|
131
|
+
const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
|
|
132
|
+
f.name.toLowerCase().startsWith(baseName.toLowerCase()));
|
|
133
|
+
if (!hasCompanion) {
|
|
134
|
+
importable = false;
|
|
135
|
+
skipReason = `Missing companion file (.${compExt})`;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let hasVideo = false;
|
|
140
|
+
const hasThumbnail = folderFiles.some(f => IMAGE_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
|
|
141
|
+
if (importable) {
|
|
142
|
+
const videoFiles = folderFiles.filter(f => VIDEO_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
|
|
143
|
+
const nonAviVideo = videoFiles.find(f => this.getExtension(f.name).toLowerCase() !== 'avi');
|
|
144
|
+
if (videoFiles.length === 0) {
|
|
145
|
+
importable = false;
|
|
146
|
+
skipReason = 'No video file found in folder';
|
|
147
|
+
}
|
|
148
|
+
else if (nonAviVideo) {
|
|
149
|
+
hasVideo = true;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
importable = false;
|
|
153
|
+
skipReason = 'Only AVI video format found; AVI is not supported';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (importable) {
|
|
157
|
+
try {
|
|
158
|
+
const fs = this.getBindings().fs;
|
|
159
|
+
const content = await fs.readFile(controlFile.uri);
|
|
160
|
+
const text = typeof content === 'string' ? content : content?.toString?.('utf8') ?? '';
|
|
161
|
+
if (this.containsAbsolutePath(text.slice(0, 4096))) {
|
|
162
|
+
importable = false;
|
|
163
|
+
skipReason = 'Route references an absolute video path';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const alreadyImported = await this.getRouteList()
|
|
170
|
+
.existsBySourceUri(controlFile.uri)
|
|
171
|
+
.catch(() => false);
|
|
172
|
+
return {
|
|
173
|
+
id: uuidv4(),
|
|
174
|
+
folderUri,
|
|
175
|
+
folderName,
|
|
176
|
+
controlFileUri: controlFile.uri,
|
|
177
|
+
format: ext,
|
|
178
|
+
hasVideo,
|
|
179
|
+
hasThumbnail,
|
|
180
|
+
alreadyImported,
|
|
181
|
+
importable,
|
|
182
|
+
skipReason
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async _ingest(routes, treeUri, observer) {
|
|
186
|
+
await waitNextTick();
|
|
187
|
+
const importable = routes.filter(r => r.importable && !r.alreadyImported);
|
|
188
|
+
const total = importable.length;
|
|
189
|
+
let imported = 0;
|
|
190
|
+
let errors = 0;
|
|
191
|
+
const failedRoutes = [];
|
|
192
|
+
for (let i = 0; i < importable.length; i++) {
|
|
193
|
+
const route = importable[i];
|
|
194
|
+
observer.emit('ingest-progress', { current: i + 1, total, currentName: route.folderName });
|
|
195
|
+
try {
|
|
196
|
+
await this.ingestRoute(route, treeUri);
|
|
197
|
+
imported++;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const reason = err?.message ?? String(err);
|
|
201
|
+
errors++;
|
|
202
|
+
failedRoutes.push({ name: route.folderName, reason });
|
|
203
|
+
observer.emit('ingest-error', { name: route.folderName, reason });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const skipped = routes.length - importable.length;
|
|
207
|
+
observer.emit('ingest-complete', { imported, skipped, errors, failedRoutes });
|
|
208
|
+
}
|
|
209
|
+
async ingestRoute(route, treeUri) {
|
|
210
|
+
const { format, controlFileUri, folderUri } = route;
|
|
211
|
+
const fileInfo = this.buildFileInfo(controlFileUri, format);
|
|
212
|
+
const { data } = await RouteParser.parse(fileInfo);
|
|
213
|
+
let thumbnailPath;
|
|
214
|
+
if (route.hasThumbnail) {
|
|
215
|
+
thumbnailPath = await this.copyThumbnail(route, folderUri).catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
const videoUri = data.videoUrl ? this.resolveVideoUri(data.videoUrl, folderUri) : undefined;
|
|
218
|
+
const record = {
|
|
219
|
+
id: data.id ?? uuidv4(),
|
|
220
|
+
name: data.title ?? route.folderName,
|
|
221
|
+
format,
|
|
222
|
+
thumbnailPath,
|
|
223
|
+
videoUri,
|
|
224
|
+
sourceTreeUri: treeUri
|
|
225
|
+
};
|
|
226
|
+
await this.getRouteList().addRoute(record);
|
|
227
|
+
}
|
|
228
|
+
async copyThumbnail(route, folderUri) {
|
|
229
|
+
const { fs, appInfo } = this.getBindings();
|
|
230
|
+
if (!fs || !appInfo)
|
|
231
|
+
return undefined;
|
|
232
|
+
const entries = await fs.readdir(folderUri, { extended: true });
|
|
233
|
+
const thumbnailFile = entries.find(e => !e.isDirectory && IMAGE_EXTENSIONS.has(this.getExtension(e.name).toLowerCase()));
|
|
234
|
+
if (!thumbnailFile)
|
|
235
|
+
return undefined;
|
|
236
|
+
const destDir = `${appInfo.getAppDir()}/thumbnails`;
|
|
237
|
+
await fs.ensureDir(destDir);
|
|
238
|
+
const srcContent = await fs.readFile(thumbnailFile.uri);
|
|
239
|
+
const destPath = `${destDir}/${route.id}.${this.getExtension(thumbnailFile.name)}`;
|
|
240
|
+
await fs.writeFile(destPath, srcContent);
|
|
241
|
+
return destPath;
|
|
242
|
+
}
|
|
243
|
+
resolveVideoUri(videoRef, folderUri) {
|
|
244
|
+
if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
|
|
245
|
+
return videoRef;
|
|
246
|
+
}
|
|
247
|
+
if (videoRef.startsWith('content://')) {
|
|
248
|
+
return videoRef;
|
|
249
|
+
}
|
|
250
|
+
if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
|
|
251
|
+
throw new Error('Absolute video path references are not supported during ingest');
|
|
252
|
+
}
|
|
253
|
+
return `${folderUri}/${videoRef}`;
|
|
254
|
+
}
|
|
255
|
+
async upsertImportHistory(folderInfo, routeCount) {
|
|
256
|
+
try {
|
|
257
|
+
const repo = JsonRepository.create('importedLibraries');
|
|
258
|
+
const names = await repo.list();
|
|
259
|
+
const all = await Promise.all((names ?? []).map(n => repo.read(n)));
|
|
260
|
+
const existing = all
|
|
261
|
+
.map(lib => lib)
|
|
262
|
+
.find(lib => lib?.treeUri === folderInfo.uri);
|
|
263
|
+
const id = existing?.id ?? uuidv4();
|
|
264
|
+
await repo.write(id, {
|
|
265
|
+
id,
|
|
266
|
+
treeUri: folderInfo.uri,
|
|
267
|
+
displayName: folderInfo.displayName,
|
|
268
|
+
lastScanned: new Date().toISOString(),
|
|
269
|
+
routeCount
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
this.logError(err, 'upsertImportHistory', { uri: folderInfo.uri });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
buildFileInfo(uri, ext) {
|
|
277
|
+
const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\'));
|
|
278
|
+
const dir = uri.slice(0, lastSlash);
|
|
279
|
+
const base = uri.slice(lastSlash + 1);
|
|
280
|
+
const name = base.slice(0, base.length - ext.length - 1);
|
|
281
|
+
return {
|
|
282
|
+
type: 'url',
|
|
283
|
+
url: uri,
|
|
284
|
+
filename: uri,
|
|
285
|
+
base,
|
|
286
|
+
name,
|
|
287
|
+
dir,
|
|
288
|
+
ext,
|
|
289
|
+
delimiter: '/'
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
getCompanionExts(parsers, primaryExt) {
|
|
293
|
+
try {
|
|
294
|
+
const matching = parsers.suppertsExtension(primaryExt);
|
|
295
|
+
const parser = matching.find(p => p.getPrimaryExtension() === primaryExt);
|
|
296
|
+
return parser?.getCompanionExtensions() ?? [];
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
getExtension(filename) {
|
|
303
|
+
const dot = filename.lastIndexOf('.');
|
|
304
|
+
return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
|
|
305
|
+
}
|
|
306
|
+
containsAbsolutePath(content) {
|
|
307
|
+
if (/[>"']\//m.test(content))
|
|
308
|
+
return true;
|
|
309
|
+
if (/[A-Za-z]:[/\\]/m.test(content))
|
|
310
|
+
return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
getRouteList() {
|
|
314
|
+
return useRouteList();
|
|
315
|
+
}
|
|
316
|
+
getBindings() {
|
|
317
|
+
return getBindings();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
return RouteLibraryScannerService = _classThis;
|
|
321
|
+
})();
|
|
322
|
+
export { RouteLibraryScannerService };
|
|
323
|
+
export const useRouteLibraryScanner = () => new RouteLibraryScannerService();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export interface ReadDirResult {
|
|
2
|
+
name: string;
|
|
3
|
+
uri: string;
|
|
4
|
+
isDirectory: boolean;
|
|
5
|
+
}
|
|
1
6
|
export interface IFileSystem {
|
|
2
7
|
writeFile(...args: any[]): any;
|
|
3
8
|
readFile(...args: any[]): any;
|
|
@@ -11,5 +16,11 @@ export interface IFileSystem {
|
|
|
11
16
|
existsDir(path: any): Promise<boolean>;
|
|
12
17
|
mkdir(path: any): Promise<void>;
|
|
13
18
|
ensureDir(path: any): Promise<void>;
|
|
14
|
-
readdir?(path: string, options
|
|
19
|
+
readdir?(path: string, options?: {
|
|
20
|
+
recursive?: boolean;
|
|
21
|
+
}): Promise<string[]>;
|
|
22
|
+
readdir?(path: string, options: {
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
extended: true;
|
|
25
|
+
}): Promise<ReadDirResult[]>;
|
|
15
26
|
}
|
|
@@ -9,6 +9,8 @@ export interface EpmParserContext extends XmlParserContext {
|
|
|
9
9
|
export declare class EPMParser extends XMLParser {
|
|
10
10
|
static readonly SCHEME = "roadmovie";
|
|
11
11
|
supportsExtension(extension: any): boolean;
|
|
12
|
+
getPrimaryExtension(): string;
|
|
13
|
+
getCompanionExtensions(): string[];
|
|
12
14
|
protected loadPoints(context: EpmParserContext): Promise<void>;
|
|
13
15
|
protected loadEpp(context: EpmParserContext): Promise<Buffer>;
|
|
14
16
|
protected getV7Program(reader: BinaryReader, json: any): DaumEpp;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Parser } from "
|
|
1
|
+
import type { Parser } from "./types";
|
|
2
2
|
export declare class ParserFactory {
|
|
3
3
|
protected static _instance: ParserFactory;
|
|
4
4
|
static getInstance(): ParserFactory;
|
|
@@ -7,6 +7,7 @@ export declare class ParserFactory {
|
|
|
7
7
|
constructor();
|
|
8
8
|
add(parser: Parser<unknown, unknown>): void;
|
|
9
9
|
suppertsExtension(extension: string): Parser<unknown, unknown>[];
|
|
10
|
+
isPrimaryExtension(extension: string): boolean;
|
|
10
11
|
findMatching(extension: string, data?: unknown): Parser<unknown, unknown>;
|
|
11
12
|
isInitialized(): boolean;
|
|
12
13
|
setInitialized(done: boolean): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FileInfo } from "../../../api";
|
|
2
|
-
import {
|
|
2
|
+
import { RouteBase, RoutePoint, VideoMapping } from "../types";
|
|
3
|
+
import type { Parser, ParseResult } from "./types";
|
|
3
4
|
export type Geometry = {
|
|
4
5
|
geometry: Array<GeometryPoint>;
|
|
5
6
|
video_points: Array<VideoPoint>;
|
|
@@ -20,6 +21,8 @@ export interface GeoParserData extends RouteBase {
|
|
|
20
21
|
}
|
|
21
22
|
export declare class GeometryParser implements Parser<Geometry, GeoParserData> {
|
|
22
23
|
import(file: FileInfo, data?: Geometry): Promise<ParseResult<GeoParserData>>;
|
|
24
|
+
getPrimaryExtension(): string;
|
|
25
|
+
getCompanionExtensions(): string[];
|
|
23
26
|
supportsExtension(extension: string): boolean;
|
|
24
27
|
supportsContent(data: Geometry): boolean;
|
|
25
28
|
getData(file: FileInfo, data?: Geometry): Promise<Geometry>;
|
|
@@ -12,6 +12,8 @@ export declare class GPXParser extends XMLParser {
|
|
|
12
12
|
protected static SCHEME: string;
|
|
13
13
|
protected props: GPXParserProps;
|
|
14
14
|
constructor(props?: GPXParserProps);
|
|
15
|
+
getPrimaryExtension(): string;
|
|
16
|
+
getCompanionExtensions(): string[];
|
|
15
17
|
supportsExtension(extension: any): boolean;
|
|
16
18
|
protected loadDescription(context: XmlParserContext): Promise<void>;
|
|
17
19
|
protected getNumberOfPoints(context: XmlParserContext): number;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export * from './kwt';
|
|
2
2
|
import { FileInfo } from '../../../api';
|
|
3
3
|
import { ParserFactory } from './factory';
|
|
4
|
-
import { ParseResult } from '../types';
|
|
5
4
|
import { RouteApiDetail } from '../api/types';
|
|
5
|
+
import type { ParseResult } from './types';
|
|
6
6
|
export declare const useParsers: () => ParserFactory;
|
|
7
7
|
export declare class RouteParser {
|
|
8
8
|
static parse(info: FileInfo): Promise<ParseResult<RouteApiDetail>>;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { FileInfo } from "../../../api";
|
|
2
2
|
import { RouteApiDetail } from "../api/types";
|
|
3
|
-
import { ParseResult, Parser } from "../types";
|
|
4
3
|
import { XmlJSON } from "../../../utils/xml";
|
|
5
4
|
import { XMLParser } from "./xml";
|
|
5
|
+
import type { ParseResult, Parser } from "./types";
|
|
6
6
|
export declare class MultipleXMLParser implements Parser<XmlJSON, RouteApiDetail> {
|
|
7
7
|
protected parsers: any;
|
|
8
8
|
constructor(classes: Array<typeof XMLParser>);
|
|
9
9
|
import(file: FileInfo, data?: XmlJSON): Promise<ParseResult<RouteApiDetail>>;
|
|
10
10
|
supportsExtension(extension: string): boolean;
|
|
11
|
+
getPrimaryExtension(): string;
|
|
12
|
+
getCompanionExtensions(): string[];
|
|
11
13
|
supportsContent(): boolean;
|
|
12
14
|
getData(info: FileInfo, data?: XmlJSON): Promise<XmlJSON>;
|
|
13
15
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { FileInfo } from "../../../../api";
|
|
2
2
|
import { RouteApiDetail } from "../../api/types";
|
|
3
3
|
import { PgmfFile, RlvCourseInfo, RlvFile } from "../../model/tacx";
|
|
4
|
-
import {
|
|
4
|
+
import { RouteInfo, RoutePoint } from "../../types";
|
|
5
|
+
import type { Parser, ParseResult } from "../types";
|
|
5
6
|
export interface TacxParserContext {
|
|
6
7
|
rlvFile: FileInfo;
|
|
7
8
|
pgmfFile: FileInfo;
|
|
@@ -13,6 +14,8 @@ export declare class TacxParser implements Parser<ArrayBuffer, RouteApiDetail> {
|
|
|
13
14
|
import(file: FileInfo, data?: ArrayBuffer): Promise<ParseResult<RouteApiDetail>>;
|
|
14
15
|
getData(info: FileInfo, data?: ArrayBuffer): Promise<ArrayBuffer>;
|
|
15
16
|
supportsContent(data: ArrayBuffer): boolean;
|
|
17
|
+
getPrimaryExtension(): string;
|
|
18
|
+
getCompanionExtensions(): string[];
|
|
16
19
|
protected buildContext(file: FileInfo): TacxParserContext;
|
|
17
20
|
protected parseRlv(context: TacxParserContext): Promise<void>;
|
|
18
21
|
protected parsePgmf(context: TacxParserContext): Promise<void>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { FileInfo } from "../../../api";
|
|
2
|
+
import { RouteBase, RouteInfo } from "../types";
|
|
1
3
|
export type Position = {
|
|
2
4
|
distance?: string;
|
|
3
5
|
lat?: string;
|
|
@@ -13,3 +15,15 @@ export type CutInfo = {
|
|
|
13
15
|
startFrame?: number;
|
|
14
16
|
endFrame?: number;
|
|
15
17
|
};
|
|
18
|
+
export interface ParseResult<T extends RouteBase> {
|
|
19
|
+
data: RouteInfo;
|
|
20
|
+
details: T;
|
|
21
|
+
}
|
|
22
|
+
export interface Parser<In, Out extends RouteBase> {
|
|
23
|
+
import(file: FileInfo, data?: In): Promise<ParseResult<Out>>;
|
|
24
|
+
supportsExtension(extension: string): boolean;
|
|
25
|
+
supportsContent(data: In): boolean;
|
|
26
|
+
getData(info: FileInfo, data?: In): Promise<In>;
|
|
27
|
+
getPrimaryExtension(): string;
|
|
28
|
+
getCompanionExtensions(): string[];
|
|
29
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { EventLogger } from 'gd-eventlog';
|
|
2
2
|
import { JSONObject, XmlJSON } from '../../../utils/xml';
|
|
3
3
|
import { RouteApiDetail } from '../api/types';
|
|
4
|
-
import {
|
|
4
|
+
import { RouteInfo } from '../types';
|
|
5
5
|
import { FileInfo } from '../../../api';
|
|
6
|
+
import type { ParseResult, Parser } from './types';
|
|
6
7
|
export interface XmlParserContext {
|
|
7
8
|
fileInfo: FileInfo;
|
|
8
9
|
data: JSONObject;
|
|
@@ -12,6 +13,8 @@ export declare class XMLParser implements Parser<XmlJSON, RouteApiDetail> {
|
|
|
12
13
|
protected logger?: EventLogger;
|
|
13
14
|
import(file: FileInfo, data?: XmlJSON): Promise<ParseResult<RouteApiDetail>>;
|
|
14
15
|
getSupportedSheme(): string;
|
|
16
|
+
getPrimaryExtension(): string;
|
|
17
|
+
getCompanionExtensions(): string[];
|
|
15
18
|
supportsExtension(extension: string): boolean;
|
|
16
19
|
supportsContent(xmljson: XmlJSON): boolean;
|
|
17
20
|
getData(file: FileInfo, data?: XmlJSON): Promise<XmlJSON>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FileInfo } from "../../../api";
|
|
2
1
|
import { LocalizedText } from "../../../i18n";
|
|
3
2
|
import { LatLng } from "../../../utils/geo";
|
|
4
3
|
export type RouteType = 'gpx' | 'video';
|
|
@@ -120,16 +119,6 @@ export interface RouteInfo extends RouteBase {
|
|
|
120
119
|
source?: string;
|
|
121
120
|
isLoopVerified?: boolean;
|
|
122
121
|
}
|
|
123
|
-
export interface ParseResult<T extends RouteBase> {
|
|
124
|
-
data: RouteInfo;
|
|
125
|
-
details: T;
|
|
126
|
-
}
|
|
127
|
-
export interface Parser<In, Out extends RouteBase> {
|
|
128
|
-
import(file: FileInfo, data?: In): Promise<ParseResult<Out>>;
|
|
129
|
-
supportsExtension(extension: string): boolean;
|
|
130
|
-
supportsContent(data: In): boolean;
|
|
131
|
-
getData(info: FileInfo, data?: In): Promise<In>;
|
|
132
|
-
}
|
|
133
122
|
export interface AppStatus {
|
|
134
123
|
isOnline?: boolean;
|
|
135
124
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IncyclistService } from '../../base/service';
|
|
2
|
+
import { IObserver } from '../../base/typedefs';
|
|
3
|
+
import { DiscoveredRoute, FolderInfo, RouteRecord } from './types';
|
|
4
|
+
interface ILibraryRouteList {
|
|
5
|
+
existsBySourceUri(uri: string): Promise<boolean>;
|
|
6
|
+
addRoute(record: RouteRecord): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare class RouteLibraryScannerService extends IncyclistService {
|
|
9
|
+
constructor();
|
|
10
|
+
scan(folderInfo: FolderInfo): IObserver;
|
|
11
|
+
ingest(routes: DiscoveredRoute[], treeUri: string): IObserver;
|
|
12
|
+
private _scan;
|
|
13
|
+
private scanFolder;
|
|
14
|
+
private buildDiscoveredRoute;
|
|
15
|
+
private _ingest;
|
|
16
|
+
private ingestRoute;
|
|
17
|
+
private copyThumbnail;
|
|
18
|
+
private resolveVideoUri;
|
|
19
|
+
private upsertImportHistory;
|
|
20
|
+
private buildFileInfo;
|
|
21
|
+
private getCompanionExts;
|
|
22
|
+
private getExtension;
|
|
23
|
+
private containsAbsolutePath;
|
|
24
|
+
protected getRouteList(): ILibraryRouteList;
|
|
25
|
+
protected getBindings(): import("../../api").IncyclistBindings;
|
|
26
|
+
}
|
|
27
|
+
export declare const useRouteLibraryScanner: () => RouteLibraryScannerService;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface FolderInfo {
|
|
2
|
+
uri: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
}
|
|
5
|
+
export interface DiscoveredRoute {
|
|
6
|
+
id: string;
|
|
7
|
+
folderUri: string;
|
|
8
|
+
folderName: string;
|
|
9
|
+
controlFileUri: string;
|
|
10
|
+
format: string;
|
|
11
|
+
hasVideo: boolean;
|
|
12
|
+
hasThumbnail: boolean;
|
|
13
|
+
alreadyImported: boolean;
|
|
14
|
+
importable: boolean;
|
|
15
|
+
skipReason?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RouteRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
format: string;
|
|
21
|
+
thumbnailPath?: string;
|
|
22
|
+
videoUri?: string;
|
|
23
|
+
sourceTreeUri: string;
|
|
24
|
+
}
|
|
25
|
+
export interface FailedRoute {
|
|
26
|
+
name: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ImportDisplayProps {
|
|
30
|
+
phase: 'landing' | 'scanning' | 'selecting' | 'ingesting' | 'complete' | 'result' | 'error';
|
|
31
|
+
discoveredRoutes: DiscoveredRoute[];
|
|
32
|
+
scanProgress?: {
|
|
33
|
+
scannedFolders: number;
|
|
34
|
+
};
|
|
35
|
+
ingestProgress?: {
|
|
36
|
+
current: number;
|
|
37
|
+
total: number;
|
|
38
|
+
currentName: string;
|
|
39
|
+
};
|
|
40
|
+
completionSummary?: {
|
|
41
|
+
imported: number;
|
|
42
|
+
skipped: number;
|
|
43
|
+
errors: number;
|
|
44
|
+
failedRoutes: FailedRoute[];
|
|
45
|
+
};
|
|
46
|
+
resultSuccess?: {
|
|
47
|
+
routeName: string;
|
|
48
|
+
};
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ImportedLibrary {
|
|
52
|
+
id: string;
|
|
53
|
+
treeUri: string;
|
|
54
|
+
displayName: string;
|
|
55
|
+
lastScanned: string;
|
|
56
|
+
routeCount: number;
|
|
57
|
+
}
|
|
@@ -4,4 +4,6 @@ export type * from './list/cards/types';
|
|
|
4
4
|
export type * from './base/api/types';
|
|
5
5
|
export type * from './base/types';
|
|
6
6
|
export type * from './base/model/types';
|
|
7
|
+
export type * from './base/parsers/types';
|
|
8
|
+
export type * from './library/types';
|
|
7
9
|
export type RouteImportStatus = 'idle' | 'parsing' | 'error' | 'success';
|