incyclist-services 1.7.87 → 1.7.88
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/tacx/TacxParser.js +1 -4
- package/lib/cjs/routes/base/parsers/utils.js +13 -2
- package/lib/cjs/routes/download/service.js +4 -2
- package/lib/cjs/routes/list/cards/RouteCard.js +1 -1
- package/lib/cjs/routes/list/loaders/db.js +41 -6
- package/lib/cjs/routes/list/loaders/types.js +24 -1
- package/lib/cjs/routes/list/service.js +7 -0
- package/lib/cjs/workouts/ride/page/service.js +6 -9
- package/lib/cjs/workouts/ride/service.js +29 -22
- package/lib/esm/routes/base/parsers/tacx/TacxParser.js +2 -5
- package/lib/esm/routes/base/parsers/utils.js +10 -1
- package/lib/esm/routes/download/service.js +4 -2
- package/lib/esm/routes/list/cards/RouteCard.js +1 -1
- package/lib/esm/routes/list/loaders/db.js +36 -1
- package/lib/esm/routes/list/loaders/types.js +22 -0
- package/lib/esm/routes/list/service.js +7 -0
- package/lib/esm/workouts/ride/page/service.js +6 -9
- package/lib/esm/workouts/ride/service.js +29 -22
- package/lib/types/routes/base/parsers/utils.d.ts +2 -0
- package/lib/types/routes/list/cards/RouteCard.d.ts +1 -1
- package/lib/types/routes/list/loaders/db.d.ts +2 -0
- package/lib/types/routes/list/loaders/types.d.ts +2 -0
- package/lib/types/workouts/ride/page/service.d.ts +0 -1
- package/lib/types/workouts/ride/service.d.ts +1 -0
- package/package.json +1 -1
|
@@ -142,10 +142,7 @@ let TacxParser = (() => {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
buildVideoUrl(filePath) {
|
|
145
|
-
|
|
146
|
-
return `video:///${filePath}`;
|
|
147
|
-
}
|
|
148
|
-
return `video://${filePath}`;
|
|
145
|
+
return (0, utils_2.buildVideoUrl)(filePath);
|
|
149
146
|
}
|
|
150
147
|
async buildResult(context) {
|
|
151
148
|
if (!context.rlvData || !context.pgmfData) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getUtf8Data = exports.fixIncorrectFileInfo = exports.parseInformations = exports.safeEncode = exports.safeDecode = exports.getReferencedFileInfo = exports.BinaryReader = void 0;
|
|
3
|
+
exports.getUtf8Data = exports.fixIncorrectFileInfo = exports.parseInformations = exports.buildFileUrl = exports.safeEncode = exports.safeDecode = exports.buildVideoUrl = exports.getReferencedFileInfo = exports.BinaryReader = void 0;
|
|
4
4
|
const api_1 = require("../../../api");
|
|
5
5
|
class BinaryReader {
|
|
6
6
|
pos;
|
|
@@ -116,6 +116,16 @@ const isAbsolutePath = (path) => {
|
|
|
116
116
|
return true;
|
|
117
117
|
return false;
|
|
118
118
|
};
|
|
119
|
+
const buildVideoUrl = (filePath) => {
|
|
120
|
+
if (isAbsolutePath(filePath)) {
|
|
121
|
+
if (/^[A-Za-z]:/.test(filePath)) {
|
|
122
|
+
return `video:///${filePath}`;
|
|
123
|
+
}
|
|
124
|
+
return `video://${filePath}`;
|
|
125
|
+
}
|
|
126
|
+
return `video://${filePath}`;
|
|
127
|
+
};
|
|
128
|
+
exports.buildVideoUrl = buildVideoUrl;
|
|
119
129
|
const safeDecode = (path) => {
|
|
120
130
|
try {
|
|
121
131
|
return decodeURIComponent(path);
|
|
@@ -144,6 +154,7 @@ const buildFileUrl = (normalizedPath) => {
|
|
|
144
154
|
}
|
|
145
155
|
return `file://${encodeURI(normalizedPath)}`;
|
|
146
156
|
};
|
|
157
|
+
exports.buildFileUrl = buildFileUrl;
|
|
147
158
|
const buildUrlFromFile = (info, referenced) => {
|
|
148
159
|
if (referenced.url) {
|
|
149
160
|
return referenced.url;
|
|
@@ -159,7 +170,7 @@ const buildUrlFromFile = (info, referenced) => {
|
|
|
159
170
|
return handleFileUrlPath(fileName);
|
|
160
171
|
}
|
|
161
172
|
const normalizedPath = (0, exports.safeDecode)(fileName);
|
|
162
|
-
return buildFileUrl(normalizedPath);
|
|
173
|
+
return (0, exports.buildFileUrl)(normalizedPath);
|
|
163
174
|
};
|
|
164
175
|
const buildAbsolutePathTarget = (fileName, info, scheme) => {
|
|
165
176
|
const inputUrl = info.url;
|
|
@@ -40,6 +40,7 @@ const decorators_1 = require("../../base/decorators");
|
|
|
40
40
|
const service_1 = require("../../base/service");
|
|
41
41
|
const settings_1 = require("../../settings");
|
|
42
42
|
const utils_1 = require("../../utils");
|
|
43
|
+
const utils_2 = require("../base/parsers/utils");
|
|
43
44
|
const api_2 = require("../list/loaders/api");
|
|
44
45
|
const db_1 = require("../list/loaders/db");
|
|
45
46
|
const types_1 = require("./types");
|
|
@@ -217,8 +218,9 @@ let RouteDownloadService = (() => {
|
|
|
217
218
|
session.on('progress', onProgress);
|
|
218
219
|
session.on('error', onError);
|
|
219
220
|
session.once('done', () => {
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
const localUrl = (0, utils_2.buildVideoUrl)(file);
|
|
222
|
+
this.logEvent({ message: 'download finished ', title, id, url, localUrl });
|
|
223
|
+
observer.emit('done', localUrl);
|
|
222
224
|
});
|
|
223
225
|
session.start();
|
|
224
226
|
}
|
|
@@ -362,7 +362,7 @@ let RouteCard = (() => {
|
|
|
362
362
|
}
|
|
363
363
|
if (descr?.hasVideo && !descr.videoUrl) {
|
|
364
364
|
const details = this.getRouteData();
|
|
365
|
-
if (details.video
|
|
365
|
+
if (details.video?.url) {
|
|
366
366
|
canStart = false;
|
|
367
367
|
videoChecking = true;
|
|
368
368
|
videoMissing = this.isVideoMissing();
|
|
@@ -41,10 +41,12 @@ 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");
|
|
44
|
+
const route_1 = require("../../base/model/route");
|
|
44
45
|
const LegacyDB_1 = require("./LegacyDB");
|
|
46
|
+
const types_2 = require("./types");
|
|
45
47
|
const DBLoader_1 = require("./DBLoader");
|
|
46
48
|
const utils_1 = require("../../../utils");
|
|
47
|
-
const
|
|
49
|
+
const route_2 = require("../../base/utils/route");
|
|
48
50
|
const clone_1 = __importDefault(require("../../../utils/clone"));
|
|
49
51
|
let RoutesDbLoader = (() => {
|
|
50
52
|
let _classDecorators = [types_1.Singleton];
|
|
@@ -126,8 +128,34 @@ let RoutesDbLoader = (() => {
|
|
|
126
128
|
if (!descr)
|
|
127
129
|
return;
|
|
128
130
|
const details = await this.loadDetailRecord(descr);
|
|
131
|
+
if (!details)
|
|
132
|
+
return details;
|
|
133
|
+
await this.correctPersistedVideoUrl(descr, details);
|
|
129
134
|
return details;
|
|
130
135
|
}
|
|
136
|
+
async correctPersistedVideoUrl(descr, details) {
|
|
137
|
+
const corrected = (0, types_2.correctVideoUrl)(details.video?.url, this.isMobile());
|
|
138
|
+
if (!corrected)
|
|
139
|
+
return false;
|
|
140
|
+
details.video.url = corrected;
|
|
141
|
+
if (descr.videoUrl !== corrected) {
|
|
142
|
+
descr.videoUrl = corrected;
|
|
143
|
+
}
|
|
144
|
+
await this.writeDetailsRecord(descr, details);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
async writeDetailsRecord(descr, details) {
|
|
148
|
+
const repo = this.getDetailsRepo(new route_1.Route(descr));
|
|
149
|
+
if (!repo)
|
|
150
|
+
return;
|
|
151
|
+
const id = descr.legacyId || descr.id;
|
|
152
|
+
try {
|
|
153
|
+
await repo.write(id, details);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this.logger.logEvent({ message: 'could not persist corrected video URL', id, error: err.message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
131
159
|
async writeRepo() {
|
|
132
160
|
if (this.saveObserver)
|
|
133
161
|
await this.saveObserver.wait();
|
|
@@ -260,20 +288,27 @@ let RoutesDbLoader = (() => {
|
|
|
260
288
|
if (details.localizedTitle && typeof (details.localizedTitle) === 'string') {
|
|
261
289
|
details.localizedTitle = { en: details.localizedTitle };
|
|
262
290
|
}
|
|
263
|
-
(0,
|
|
264
|
-
(0,
|
|
291
|
+
(0, route_2.validateDistance)(details.points);
|
|
292
|
+
(0, route_2.validateSlopes)(details.points);
|
|
265
293
|
}
|
|
266
294
|
validateDescription(description, details) {
|
|
267
295
|
if (!description.elevation) {
|
|
268
|
-
description.elevation = (0,
|
|
296
|
+
description.elevation = (0, route_2.getTotalElevation)(details);
|
|
269
297
|
}
|
|
270
298
|
}
|
|
271
299
|
async loadDetails(route, alreadyAdded) {
|
|
272
300
|
const details = await this.loadDetailRecord(route);
|
|
273
301
|
let updated = false;
|
|
274
|
-
|
|
302
|
+
const verifyVideoUrl = (route) => {
|
|
303
|
+
const res = this.verifyVideoUrl(route);
|
|
304
|
+
if (res) {
|
|
305
|
+
this.logger.logEvent({ message: 'video URL updated', url: route?.details?.video?.url });
|
|
306
|
+
}
|
|
307
|
+
return res;
|
|
308
|
+
};
|
|
309
|
+
(0, route_2.addDetails)(route, details);
|
|
275
310
|
updated ||= this.verifyRouteHash(route);
|
|
276
|
-
updated ||=
|
|
311
|
+
updated ||= verifyVideoUrl(route);
|
|
277
312
|
if (alreadyAdded)
|
|
278
313
|
this.emitRouteUpdate(route);
|
|
279
314
|
this.emitRouteAdded(route);
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Loader = void 0;
|
|
3
|
+
exports.Loader = exports.correctVideoUrl = void 0;
|
|
4
|
+
const api_1 = require("../../../api");
|
|
4
5
|
const utils_1 = require("../../../utils");
|
|
5
6
|
const valid_1 = require("../../../utils/valid");
|
|
7
|
+
const utils_2 = require("../../base/parsers/utils");
|
|
6
8
|
const route_1 = require("../../base/utils/route");
|
|
9
|
+
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
10
|
+
const correctVideoUrl = (url, isMobile = false) => {
|
|
11
|
+
if (!url || !url.startsWith('video:'))
|
|
12
|
+
return undefined;
|
|
13
|
+
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
14
|
+
if (!malformed && !isMobile)
|
|
15
|
+
return undefined;
|
|
16
|
+
const rawPath = malformed
|
|
17
|
+
? url.replace(/^video:\/{3}/, '')
|
|
18
|
+
: url.replace(/^video:\/{2}/, '');
|
|
19
|
+
return isMobile ? (0, utils_2.buildFileUrl)(rawPath) : (0, utils_2.buildVideoUrl)(rawPath);
|
|
20
|
+
};
|
|
21
|
+
exports.correctVideoUrl = correctVideoUrl;
|
|
7
22
|
class Loader {
|
|
8
23
|
loadObserver;
|
|
9
24
|
isCompleted(route) {
|
|
@@ -15,6 +30,9 @@ class Loader {
|
|
|
15
30
|
}
|
|
16
31
|
return true;
|
|
17
32
|
}
|
|
33
|
+
isMobile() {
|
|
34
|
+
return (0, api_1.getBindings)()?.appInfo?.getChannel() === 'mobile';
|
|
35
|
+
}
|
|
18
36
|
verifyRouteHash(route) {
|
|
19
37
|
const { description, details } = route;
|
|
20
38
|
const prev = description.routeHash;
|
|
@@ -35,6 +53,11 @@ class Loader {
|
|
|
35
53
|
return;
|
|
36
54
|
if (descr.videoUrl) {
|
|
37
55
|
const details = route.details;
|
|
56
|
+
const corrected = (0, exports.correctVideoUrl)(details.video.url, this.isMobile());
|
|
57
|
+
if (corrected) {
|
|
58
|
+
details.video.url = corrected;
|
|
59
|
+
updated = true;
|
|
60
|
+
}
|
|
38
61
|
if (details.video.url.startsWith('video:') && !descr.isDownloaded && !descr.isLocal) {
|
|
39
62
|
descr.isDownloaded = true;
|
|
40
63
|
updated = true;
|
|
@@ -892,6 +892,13 @@ let RouteListService = (() => {
|
|
|
892
892
|
this.db.save(route, false);
|
|
893
893
|
});
|
|
894
894
|
}
|
|
895
|
+
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
896
|
+
card.videoExists().then(exists => {
|
|
897
|
+
if (!exists) {
|
|
898
|
+
card.resetDownload();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
895
902
|
})
|
|
896
903
|
.catch(() => {
|
|
897
904
|
});
|
|
@@ -167,7 +167,7 @@ let WorkoutRidePageService = (() => {
|
|
|
167
167
|
const current = this.getRideDisplay().getDisplayProperties().workout;
|
|
168
168
|
return {
|
|
169
169
|
...base,
|
|
170
|
-
title:
|
|
170
|
+
title: wo.title ?? '',
|
|
171
171
|
graph: this.buildGraphPlan(current, wo.ftp),
|
|
172
172
|
steps: this.buildUpcomingSteps(current, wo.ftp),
|
|
173
173
|
dashboard: this.buildDashboardLine(wo),
|
|
@@ -452,20 +452,17 @@ let WorkoutRidePageService = (() => {
|
|
|
452
452
|
const hasMore = currentIndex + 1 + UPCOMING_STEPS_COUNT < flattened.length;
|
|
453
453
|
return { previous, current: currentStep, upcoming, hasMore };
|
|
454
454
|
}
|
|
455
|
-
getMobileStepTitle(wo) {
|
|
456
|
-
const title = wo.title ?? '';
|
|
457
|
-
const name = wo.workout?.name;
|
|
458
|
-
const prefix = name ? `${name}: ` : '';
|
|
459
|
-
return prefix && title.startsWith(prefix) ? title.slice(prefix.length) : title;
|
|
460
|
-
}
|
|
461
455
|
buildDashboardLine(wo) {
|
|
462
456
|
const limits = this.getWorkoutRide().getCurrentLimits();
|
|
463
|
-
const title =
|
|
457
|
+
const title = wo.title ?? '';
|
|
464
458
|
if (!limits)
|
|
465
459
|
return { text: title, mode: wo.mode ?? null };
|
|
466
460
|
const target = (0, graph_1.getStepTargetText)(limits.step ?? {}, wo.ftp);
|
|
467
461
|
const duration = (0, graph_1.getStepDuration)({ duration: limits.duration });
|
|
468
|
-
const
|
|
462
|
+
const base = `${target} for ${duration}`;
|
|
463
|
+
let text = base;
|
|
464
|
+
if (title && title !== base)
|
|
465
|
+
text = title.startsWith('(') ? `${base}${title}` : `${base} - ${title}`;
|
|
469
466
|
return { text, mode: wo.mode ?? null };
|
|
470
467
|
}
|
|
471
468
|
getStepAbsolutePower(step, ftp) {
|
|
@@ -35,6 +35,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
35
35
|
};
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.getWorkoutRide = exports.useWorkoutRide = exports.WorkoutRide = void 0;
|
|
38
|
+
const api_1 = require("../../api");
|
|
38
39
|
const service_1 = require("../../base/service");
|
|
39
40
|
const types_1 = require("../../base/types");
|
|
40
41
|
const observer_1 = require("../../base/types/observer");
|
|
@@ -42,6 +43,7 @@ const devices_1 = require("../../devices");
|
|
|
42
43
|
const settings_1 = require("../../settings");
|
|
43
44
|
const utils_1 = require("../../utils");
|
|
44
45
|
const valid_1 = require("../../utils/valid");
|
|
46
|
+
const graph_1 = require("../base/graph");
|
|
45
47
|
const list_1 = require("../list");
|
|
46
48
|
const decorators_1 = require("../../base/decorators");
|
|
47
49
|
const DEFAULT_FTP = 200;
|
|
@@ -521,35 +523,40 @@ let WorkoutRide = (() => {
|
|
|
521
523
|
}
|
|
522
524
|
return { start, stop };
|
|
523
525
|
}
|
|
526
|
+
getBindings() {
|
|
527
|
+
return (0, api_1.getBindings)();
|
|
528
|
+
}
|
|
524
529
|
getStepTitle(time) {
|
|
525
530
|
if (!this.workout)
|
|
526
531
|
return;
|
|
527
|
-
let title = this.workout.name;
|
|
528
532
|
const limit = this.workout.getLimits(time, true);
|
|
529
533
|
const segment = this.workout.getSegment(time);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
ch = ' - ';
|
|
534
|
-
}
|
|
535
|
-
if (segment?.text && segment?.repeat > 0) {
|
|
536
|
-
const repeatTime = segment.duration / segment.repeat;
|
|
537
|
-
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
538
|
-
title += `(${repeatCount}/${segment.repeat})`;
|
|
539
|
-
}
|
|
540
|
-
if (!limit) {
|
|
541
|
-
title += `${ch}free`;
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
if (limit?.text)
|
|
545
|
-
title += `${ch}${limit.text}`;
|
|
546
|
-
if (limit?.text && segment && !segment.text && segment.repeat > 0) {
|
|
534
|
+
const isMobile = this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
535
|
+
const repeatSuffix = (segment?.repeat > 0)
|
|
536
|
+
? (() => {
|
|
547
537
|
const repeatTime = segment.duration / segment.repeat;
|
|
548
538
|
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
539
|
+
return `(${repeatCount}/${segment.repeat})`;
|
|
540
|
+
})()
|
|
541
|
+
: '';
|
|
542
|
+
const compose = (body) => isMobile ? body : `${this.workout.name}: ${body}`;
|
|
543
|
+
if (!limit)
|
|
544
|
+
return compose('free');
|
|
545
|
+
const segmentName = segment?.text;
|
|
546
|
+
const stepName = limit?.text;
|
|
547
|
+
if (segmentName && stepName)
|
|
548
|
+
return compose(`${segmentName}${repeatSuffix} - ${stepName}`);
|
|
549
|
+
if (segmentName)
|
|
550
|
+
return compose(`${segmentName}${repeatSuffix}`);
|
|
551
|
+
if (stepName)
|
|
552
|
+
return compose(`${stepName}${repeatSuffix}`);
|
|
553
|
+
const rawStep = this.currentLimits?.step ?? limit;
|
|
554
|
+
const verbalDescription = `${(0, graph_1.getStepTargetText)(rawStep, this.settings?.ftp)} for ${(0, graph_1.getStepDuration)(rawStep)}`;
|
|
555
|
+
if (isMobile)
|
|
556
|
+
return repeatSuffix || verbalDescription;
|
|
557
|
+
if (!segment)
|
|
558
|
+
return this.workout.name;
|
|
559
|
+
return compose(`${verbalDescription}${repeatSuffix}`);
|
|
553
560
|
}
|
|
554
561
|
getPowerVal(power, key) {
|
|
555
562
|
if (power === undefined)
|
|
@@ -40,7 +40,7 @@ import { getRouteHash, validateRoute } from "../../utils/route";
|
|
|
40
40
|
import { PGMFFileReader } from "./pgmf";
|
|
41
41
|
import { RLVFileReader } from "./rlv";
|
|
42
42
|
import { TacxFileReader } from "./TacxReader";
|
|
43
|
-
import { fixIncorrectFileInfo } from "../utils";
|
|
43
|
+
import { buildVideoUrl, fixIncorrectFileInfo } from "../utils";
|
|
44
44
|
import { EventLogger } from "gd-eventlog";
|
|
45
45
|
import { Injectable } from "../../../../base/decorators";
|
|
46
46
|
let TacxParser = (() => {
|
|
@@ -136,10 +136,7 @@ let TacxParser = (() => {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
buildVideoUrl(filePath) {
|
|
139
|
-
|
|
140
|
-
return `video:///${filePath}`;
|
|
141
|
-
}
|
|
142
|
-
return `video://${filePath}`;
|
|
139
|
+
return buildVideoUrl(filePath);
|
|
143
140
|
}
|
|
144
141
|
async buildResult(context) {
|
|
145
142
|
if (!context.rlvData || !context.pgmfData) {
|
|
@@ -111,6 +111,15 @@ const isAbsolutePath = (path) => {
|
|
|
111
111
|
return true;
|
|
112
112
|
return false;
|
|
113
113
|
};
|
|
114
|
+
export const buildVideoUrl = (filePath) => {
|
|
115
|
+
if (isAbsolutePath(filePath)) {
|
|
116
|
+
if (/^[A-Za-z]:/.test(filePath)) {
|
|
117
|
+
return `video:///${filePath}`;
|
|
118
|
+
}
|
|
119
|
+
return `video://${filePath}`;
|
|
120
|
+
}
|
|
121
|
+
return `video://${filePath}`;
|
|
122
|
+
};
|
|
114
123
|
export const safeDecode = (path) => {
|
|
115
124
|
try {
|
|
116
125
|
return decodeURIComponent(path);
|
|
@@ -128,7 +137,7 @@ const handleFileUrlPath = (fileName) => {
|
|
|
128
137
|
const cleanPath = safeDecode(fileName.replace(/^file:\/\/\/?/, ''));
|
|
129
138
|
return `file://${encodeURI(cleanPath)}`;
|
|
130
139
|
};
|
|
131
|
-
const buildFileUrl = (normalizedPath) => {
|
|
140
|
+
export const buildFileUrl = (normalizedPath) => {
|
|
132
141
|
if (isAbsolutePath(normalizedPath)) {
|
|
133
142
|
if (/^[A-Za-z]:/.exec(normalizedPath)) {
|
|
134
143
|
return `file:///${encodeURI(normalizedPath)}`;
|
|
@@ -37,6 +37,7 @@ import { Injectable, Singleton } from "../../base/decorators";
|
|
|
37
37
|
import { IncyclistService } from "../../base/service";
|
|
38
38
|
import { useUserSettings } from "../../settings";
|
|
39
39
|
import { waitNextTick } from "../../utils";
|
|
40
|
+
import { buildVideoUrl } from "../base/parsers/utils";
|
|
40
41
|
import { RoutesApiLoader } from "../list/loaders/api";
|
|
41
42
|
import { RoutesDbLoader } from "../list/loaders/db";
|
|
42
43
|
import { DownloadObserver } from "./types";
|
|
@@ -214,8 +215,9 @@ let RouteDownloadService = (() => {
|
|
|
214
215
|
session.on('progress', onProgress);
|
|
215
216
|
session.on('error', onError);
|
|
216
217
|
session.once('done', () => {
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
const localUrl = buildVideoUrl(file);
|
|
219
|
+
this.logEvent({ message: 'download finished ', title, id, url, localUrl });
|
|
220
|
+
observer.emit('done', localUrl);
|
|
219
221
|
});
|
|
220
222
|
session.start();
|
|
221
223
|
}
|
|
@@ -359,7 +359,7 @@ let RouteCard = (() => {
|
|
|
359
359
|
}
|
|
360
360
|
if (descr?.hasVideo && !descr.videoUrl) {
|
|
361
361
|
const details = this.getRouteData();
|
|
362
|
-
if (details.video
|
|
362
|
+
if (details.video?.url) {
|
|
363
363
|
canStart = false;
|
|
364
364
|
videoChecking = true;
|
|
365
365
|
videoMissing = this.isVideoMissing();
|
|
@@ -35,7 +35,9 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
35
35
|
import { JsonRepository } from "../../../api";
|
|
36
36
|
import { Singleton } from "../../../base/types";
|
|
37
37
|
import { PromiseObserver } from "../../../base/types/observer";
|
|
38
|
+
import { Route } from "../../base/model/route";
|
|
38
39
|
import { RoutesLegacyDbLoader } from "./LegacyDB";
|
|
40
|
+
import { correctVideoUrl } from "./types";
|
|
39
41
|
import { DBLoader } from "./DBLoader";
|
|
40
42
|
import { waitNextTick } from "../../../utils";
|
|
41
43
|
import { addDetails, getTotalElevation, validateDistance, validateSlopes } from "../../base/utils/route";
|
|
@@ -120,8 +122,34 @@ let RoutesDbLoader = (() => {
|
|
|
120
122
|
if (!descr)
|
|
121
123
|
return;
|
|
122
124
|
const details = await this.loadDetailRecord(descr);
|
|
125
|
+
if (!details)
|
|
126
|
+
return details;
|
|
127
|
+
await this.correctPersistedVideoUrl(descr, details);
|
|
123
128
|
return details;
|
|
124
129
|
}
|
|
130
|
+
async correctPersistedVideoUrl(descr, details) {
|
|
131
|
+
const corrected = correctVideoUrl(details.video?.url, this.isMobile());
|
|
132
|
+
if (!corrected)
|
|
133
|
+
return false;
|
|
134
|
+
details.video.url = corrected;
|
|
135
|
+
if (descr.videoUrl !== corrected) {
|
|
136
|
+
descr.videoUrl = corrected;
|
|
137
|
+
}
|
|
138
|
+
await this.writeDetailsRecord(descr, details);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
async writeDetailsRecord(descr, details) {
|
|
142
|
+
const repo = this.getDetailsRepo(new Route(descr));
|
|
143
|
+
if (!repo)
|
|
144
|
+
return;
|
|
145
|
+
const id = descr.legacyId || descr.id;
|
|
146
|
+
try {
|
|
147
|
+
await repo.write(id, details);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
this.logger.logEvent({ message: 'could not persist corrected video URL', id, error: err.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
125
153
|
async writeRepo() {
|
|
126
154
|
if (this.saveObserver)
|
|
127
155
|
await this.saveObserver.wait();
|
|
@@ -265,9 +293,16 @@ let RoutesDbLoader = (() => {
|
|
|
265
293
|
async loadDetails(route, alreadyAdded) {
|
|
266
294
|
const details = await this.loadDetailRecord(route);
|
|
267
295
|
let updated = false;
|
|
296
|
+
const verifyVideoUrl = (route) => {
|
|
297
|
+
const res = this.verifyVideoUrl(route);
|
|
298
|
+
if (res) {
|
|
299
|
+
this.logger.logEvent({ message: 'video URL updated', url: route?.details?.video?.url });
|
|
300
|
+
}
|
|
301
|
+
return res;
|
|
302
|
+
};
|
|
268
303
|
addDetails(route, details);
|
|
269
304
|
updated ||= this.verifyRouteHash(route);
|
|
270
|
-
updated ||=
|
|
305
|
+
updated ||= verifyVideoUrl(route);
|
|
271
306
|
if (alreadyAdded)
|
|
272
307
|
this.emitRouteUpdate(route);
|
|
273
308
|
this.emitRouteAdded(route);
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import { getBindings } from "../../../api";
|
|
1
2
|
import { waitNextTick } from "../../../utils";
|
|
2
3
|
import { valid } from "../../../utils/valid";
|
|
4
|
+
import { buildFileUrl, buildVideoUrl } from "../../base/parsers/utils";
|
|
3
5
|
import { getRouteHash } from "../../base/utils/route";
|
|
6
|
+
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
7
|
+
export const correctVideoUrl = (url, isMobile = false) => {
|
|
8
|
+
if (!url || !url.startsWith('video:'))
|
|
9
|
+
return undefined;
|
|
10
|
+
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
11
|
+
if (!malformed && !isMobile)
|
|
12
|
+
return undefined;
|
|
13
|
+
const rawPath = malformed
|
|
14
|
+
? url.replace(/^video:\/{3}/, '')
|
|
15
|
+
: url.replace(/^video:\/{2}/, '');
|
|
16
|
+
return isMobile ? buildFileUrl(rawPath) : buildVideoUrl(rawPath);
|
|
17
|
+
};
|
|
4
18
|
export class Loader {
|
|
5
19
|
loadObserver;
|
|
6
20
|
isCompleted(route) {
|
|
@@ -12,6 +26,9 @@ export class Loader {
|
|
|
12
26
|
}
|
|
13
27
|
return true;
|
|
14
28
|
}
|
|
29
|
+
isMobile() {
|
|
30
|
+
return getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
31
|
+
}
|
|
15
32
|
verifyRouteHash(route) {
|
|
16
33
|
const { description, details } = route;
|
|
17
34
|
const prev = description.routeHash;
|
|
@@ -32,6 +49,11 @@ export class Loader {
|
|
|
32
49
|
return;
|
|
33
50
|
if (descr.videoUrl) {
|
|
34
51
|
const details = route.details;
|
|
52
|
+
const corrected = correctVideoUrl(details.video.url, this.isMobile());
|
|
53
|
+
if (corrected) {
|
|
54
|
+
details.video.url = corrected;
|
|
55
|
+
updated = true;
|
|
56
|
+
}
|
|
35
57
|
if (details.video.url.startsWith('video:') && !descr.isDownloaded && !descr.isLocal) {
|
|
36
58
|
descr.isDownloaded = true;
|
|
37
59
|
updated = true;
|
|
@@ -886,6 +886,13 @@ let RouteListService = (() => {
|
|
|
886
886
|
this.db.save(route, false);
|
|
887
887
|
});
|
|
888
888
|
}
|
|
889
|
+
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
890
|
+
card.videoExists().then(exists => {
|
|
891
|
+
if (!exists) {
|
|
892
|
+
card.resetDownload();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
889
896
|
})
|
|
890
897
|
.catch(() => {
|
|
891
898
|
});
|
|
@@ -164,7 +164,7 @@ let WorkoutRidePageService = (() => {
|
|
|
164
164
|
const current = this.getRideDisplay().getDisplayProperties().workout;
|
|
165
165
|
return {
|
|
166
166
|
...base,
|
|
167
|
-
title:
|
|
167
|
+
title: wo.title ?? '',
|
|
168
168
|
graph: this.buildGraphPlan(current, wo.ftp),
|
|
169
169
|
steps: this.buildUpcomingSteps(current, wo.ftp),
|
|
170
170
|
dashboard: this.buildDashboardLine(wo),
|
|
@@ -449,20 +449,17 @@ let WorkoutRidePageService = (() => {
|
|
|
449
449
|
const hasMore = currentIndex + 1 + UPCOMING_STEPS_COUNT < flattened.length;
|
|
450
450
|
return { previous, current: currentStep, upcoming, hasMore };
|
|
451
451
|
}
|
|
452
|
-
getMobileStepTitle(wo) {
|
|
453
|
-
const title = wo.title ?? '';
|
|
454
|
-
const name = wo.workout?.name;
|
|
455
|
-
const prefix = name ? `${name}: ` : '';
|
|
456
|
-
return prefix && title.startsWith(prefix) ? title.slice(prefix.length) : title;
|
|
457
|
-
}
|
|
458
452
|
buildDashboardLine(wo) {
|
|
459
453
|
const limits = this.getWorkoutRide().getCurrentLimits();
|
|
460
|
-
const title =
|
|
454
|
+
const title = wo.title ?? '';
|
|
461
455
|
if (!limits)
|
|
462
456
|
return { text: title, mode: wo.mode ?? null };
|
|
463
457
|
const target = getStepTargetText(limits.step ?? {}, wo.ftp);
|
|
464
458
|
const duration = getStepDuration({ duration: limits.duration });
|
|
465
|
-
const
|
|
459
|
+
const base = `${target} for ${duration}`;
|
|
460
|
+
let text = base;
|
|
461
|
+
if (title && title !== base)
|
|
462
|
+
text = title.startsWith('(') ? `${base}${title}` : `${base} - ${title}`;
|
|
466
463
|
return { text, mode: wo.mode ?? null };
|
|
467
464
|
}
|
|
468
465
|
getStepAbsolutePower(step, ftp) {
|
|
@@ -32,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
+
import { getBindings } from "../../api";
|
|
35
36
|
import { IncyclistService } from "../../base/service";
|
|
36
37
|
import { Singleton } from "../../base/types";
|
|
37
38
|
import { Observer } from "../../base/types/observer";
|
|
@@ -39,6 +40,7 @@ import { useDeviceRide } from "../../devices";
|
|
|
39
40
|
import { useUserSettings } from "../../settings";
|
|
40
41
|
import { waitNextTick } from "../../utils";
|
|
41
42
|
import { valid } from "../../utils/valid";
|
|
43
|
+
import { getStepDuration, getStepTargetText } from "../base/graph";
|
|
42
44
|
import { useWorkoutList } from "../list";
|
|
43
45
|
import { Injectable } from "../../base/decorators";
|
|
44
46
|
const DEFAULT_FTP = 200;
|
|
@@ -518,35 +520,40 @@ let WorkoutRide = (() => {
|
|
|
518
520
|
}
|
|
519
521
|
return { start, stop };
|
|
520
522
|
}
|
|
523
|
+
getBindings() {
|
|
524
|
+
return getBindings();
|
|
525
|
+
}
|
|
521
526
|
getStepTitle(time) {
|
|
522
527
|
if (!this.workout)
|
|
523
528
|
return;
|
|
524
|
-
let title = this.workout.name;
|
|
525
529
|
const limit = this.workout.getLimits(time, true);
|
|
526
530
|
const segment = this.workout.getSegment(time);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
ch = ' - ';
|
|
531
|
-
}
|
|
532
|
-
if (segment?.text && segment?.repeat > 0) {
|
|
533
|
-
const repeatTime = segment.duration / segment.repeat;
|
|
534
|
-
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
535
|
-
title += `(${repeatCount}/${segment.repeat})`;
|
|
536
|
-
}
|
|
537
|
-
if (!limit) {
|
|
538
|
-
title += `${ch}free`;
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
if (limit?.text)
|
|
542
|
-
title += `${ch}${limit.text}`;
|
|
543
|
-
if (limit?.text && segment && !segment.text && segment.repeat > 0) {
|
|
531
|
+
const isMobile = this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
532
|
+
const repeatSuffix = (segment?.repeat > 0)
|
|
533
|
+
? (() => {
|
|
544
534
|
const repeatTime = segment.duration / segment.repeat;
|
|
545
535
|
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
536
|
+
return `(${repeatCount}/${segment.repeat})`;
|
|
537
|
+
})()
|
|
538
|
+
: '';
|
|
539
|
+
const compose = (body) => isMobile ? body : `${this.workout.name}: ${body}`;
|
|
540
|
+
if (!limit)
|
|
541
|
+
return compose('free');
|
|
542
|
+
const segmentName = segment?.text;
|
|
543
|
+
const stepName = limit?.text;
|
|
544
|
+
if (segmentName && stepName)
|
|
545
|
+
return compose(`${segmentName}${repeatSuffix} - ${stepName}`);
|
|
546
|
+
if (segmentName)
|
|
547
|
+
return compose(`${segmentName}${repeatSuffix}`);
|
|
548
|
+
if (stepName)
|
|
549
|
+
return compose(`${stepName}${repeatSuffix}`);
|
|
550
|
+
const rawStep = this.currentLimits?.step ?? limit;
|
|
551
|
+
const verbalDescription = `${getStepTargetText(rawStep, this.settings?.ftp)} for ${getStepDuration(rawStep)}`;
|
|
552
|
+
if (isMobile)
|
|
553
|
+
return repeatSuffix || verbalDescription;
|
|
554
|
+
if (!segment)
|
|
555
|
+
return this.workout.name;
|
|
556
|
+
return compose(`${verbalDescription}${repeatSuffix}`);
|
|
550
557
|
}
|
|
551
558
|
getPowerVal(power, key) {
|
|
552
559
|
if (power === undefined)
|
|
@@ -21,8 +21,10 @@ export declare const getReferencedFileInfo: (info: FileInfo, referenced: {
|
|
|
21
21
|
file?: string;
|
|
22
22
|
url?: string;
|
|
23
23
|
}, scheme?: string) => string;
|
|
24
|
+
export declare const buildVideoUrl: (filePath: string) => string;
|
|
24
25
|
export declare const safeDecode: (path: string) => string;
|
|
25
26
|
export declare const safeEncode: (path: string) => string;
|
|
27
|
+
export declare const buildFileUrl: (normalizedPath: string) => string;
|
|
26
28
|
export declare const parseInformations: (informations?: Array<JSONObject>) => Array<RouteInfoText>;
|
|
27
29
|
export declare const fixIncorrectFileInfo: (file: Partial<FileInfo>) => void;
|
|
28
30
|
export declare const getUtf8Data: (res: string | Buffer) => string;
|
|
@@ -107,7 +107,7 @@ export declare class RouteCard extends BaseCard implements Card<Route> {
|
|
|
107
107
|
protected deleteRouteUserSettings(): void;
|
|
108
108
|
protected deleteFromUIList(): void;
|
|
109
109
|
protected markDeleted(): Promise<void>;
|
|
110
|
-
|
|
110
|
+
resetDownload(): Promise<void>;
|
|
111
111
|
protected deleteRoute(): Promise<void>;
|
|
112
112
|
protected logError(err: Error, fn: string): void;
|
|
113
113
|
protected getSettings(): RouteSettings;
|
|
@@ -19,6 +19,8 @@ export declare class RoutesDbLoader extends DBLoader<RouteInfoDBEntry> {
|
|
|
19
19
|
delete(route: Route): Promise<void>;
|
|
20
20
|
getDescription(id: string): RouteInfo;
|
|
21
21
|
getDetails(id: string): Promise<RouteApiDetail>;
|
|
22
|
+
protected correctPersistedVideoUrl(descr: RouteInfo, details: RouteApiDetail): Promise<boolean>;
|
|
23
|
+
protected writeDetailsRecord(descr: RouteInfo, details: RouteApiDetail): Promise<void>;
|
|
22
24
|
protected writeRepo(): Promise<void>;
|
|
23
25
|
protected write(): void;
|
|
24
26
|
protected getLegacyLoader(): RoutesLegacyDbLoader;
|
|
@@ -2,6 +2,7 @@ import { Observer } from "../../../base/types/observer";
|
|
|
2
2
|
import { RouteApiDescription } from "../../base/api/types";
|
|
3
3
|
import { Route } from "../../base/model/route";
|
|
4
4
|
import { RouteInfo } from "../../base/types";
|
|
5
|
+
export declare const correctVideoUrl: (url?: string, isMobile?: boolean) => string | undefined;
|
|
5
6
|
export interface RouteInfoDBEntry extends RouteInfo {
|
|
6
7
|
pointsEncoded?: string;
|
|
7
8
|
}
|
|
@@ -21,6 +22,7 @@ export declare abstract class Loader<T extends MinimalDescription> {
|
|
|
21
22
|
abstract save(route: Route): Promise<void>;
|
|
22
23
|
protected abstract buildRouteInfo(descr: T): RouteInfo;
|
|
23
24
|
protected isCompleted(route: Route): boolean;
|
|
25
|
+
protected isMobile(): boolean;
|
|
24
26
|
protected verifyRouteHash(route: Route): boolean;
|
|
25
27
|
protected verifyVideoUrl(route: Route): boolean;
|
|
26
28
|
protected verifyCountry(route: Route): Promise<void>;
|
|
@@ -57,7 +57,6 @@ export declare class WorkoutRidePageService extends IncyclistPageService impleme
|
|
|
57
57
|
protected getLoadIncrement(): number;
|
|
58
58
|
protected buildGraphPlan(current: Workout | undefined, ftp: number): WorkoutGraphPlan;
|
|
59
59
|
protected buildUpcomingSteps(current: Workout | undefined, ftp: number): WorkoutUpcomingSteps;
|
|
60
|
-
protected getMobileStepTitle(wo: WorkoutDisplayProperties): string;
|
|
61
60
|
protected buildDashboardLine(wo: WorkoutDisplayProperties): WorkoutDashboardLine;
|
|
62
61
|
protected getStepAbsolutePower(step: StepDefinition, ftp?: number): number | null;
|
|
63
62
|
protected getStepFlags(): {
|
|
@@ -56,6 +56,7 @@ export declare class WorkoutRide extends IncyclistService {
|
|
|
56
56
|
start: any;
|
|
57
57
|
stop: any;
|
|
58
58
|
};
|
|
59
|
+
protected getBindings(): import("../../api").IncyclistBindings;
|
|
59
60
|
protected getStepTitle(time: number): string;
|
|
60
61
|
protected getPowerVal(power: PowerLimit, key: 'min' | 'max'): number;
|
|
61
62
|
protected getFtpFromUserSettings(): any;
|