@epic-web/workshop-utils 6.55.1 → 6.56.0
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/dist/compile-mdx.server.js +80 -63
- package/dist/db.server.d.ts +31 -0
- package/dist/db.server.js +12 -0
- package/dist/offline-videos.server.js +42 -11
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import "./init-env.js";
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { rehypeCodeBlocksShiki } from '@kentcdodds/md-temp';
|
|
5
|
+
import * as cookie from 'cookie';
|
|
5
6
|
import lz from 'lz-string';
|
|
6
7
|
import md5 from 'md5-hex';
|
|
7
8
|
import { bundleMDX } from 'mdx-bundler';
|
|
@@ -12,7 +13,51 @@ import gfm from 'remark-gfm';
|
|
|
12
13
|
import { visit } from 'unist-util-visit';
|
|
13
14
|
import { cachified, compiledInstructionMarkdownCache, compiledMarkdownCache, shouldForceFresh, } from "./cache.server.js";
|
|
14
15
|
import { checkConnection } from "./utils.server.js";
|
|
15
|
-
|
|
16
|
+
const themeCookieName = 'EpicShop_theme';
|
|
17
|
+
const themeHintCookieName = 'EpicShop_CH-prefers-color-scheme';
|
|
18
|
+
function getMermaidTheme(request) {
|
|
19
|
+
if (!request)
|
|
20
|
+
return 'default';
|
|
21
|
+
const cookieHeader = request.headers.get('cookie');
|
|
22
|
+
if (!cookieHeader)
|
|
23
|
+
return 'default';
|
|
24
|
+
const parsed = cookie.parse(cookieHeader);
|
|
25
|
+
const themeCookie = parsed[themeCookieName];
|
|
26
|
+
if (themeCookie === 'dark')
|
|
27
|
+
return 'dark';
|
|
28
|
+
if (themeCookie === 'light')
|
|
29
|
+
return 'default';
|
|
30
|
+
const hintTheme = parsed[themeHintCookieName];
|
|
31
|
+
return hintTheme === 'dark' ? 'dark' : 'default';
|
|
32
|
+
}
|
|
33
|
+
function mdxStringExpressionAttribute(name, value) {
|
|
34
|
+
return {
|
|
35
|
+
type: 'mdxJsxAttribute',
|
|
36
|
+
name,
|
|
37
|
+
value: {
|
|
38
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
39
|
+
value: JSON.stringify(value),
|
|
40
|
+
// This hack brought to you by this: https://github.com/syntax-tree/hast-util-to-estree/blob/e5ccb97e9f42bba90359ea6d0f83a11d74e0dad6/lib/handlers/mdx-expression.js#L35-L38
|
|
41
|
+
// no idea why we're required to have estree here, but I'm pretty sure someone is supposed to add it automatically for us and it just never happens...
|
|
42
|
+
data: {
|
|
43
|
+
estree: {
|
|
44
|
+
type: 'Program',
|
|
45
|
+
sourceType: 'script',
|
|
46
|
+
body: [
|
|
47
|
+
{
|
|
48
|
+
type: 'ExpressionStatement',
|
|
49
|
+
expression: {
|
|
50
|
+
type: 'Literal',
|
|
51
|
+
value,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function remarkMermaidCodeToSvg({ theme }) {
|
|
16
61
|
return async (tree) => {
|
|
17
62
|
const promises = [];
|
|
18
63
|
visit(tree, 'code', (node, index, parent) => {
|
|
@@ -21,79 +66,50 @@ function remarkMermaidCodeToSvg() {
|
|
|
21
66
|
const isConnected = await checkConnection();
|
|
22
67
|
if (isConnected) {
|
|
23
68
|
const compressed = lz.compressToEncodedURIComponent(node.value);
|
|
24
|
-
const url =
|
|
69
|
+
const url = new URL('https://mermaid-to-svg.kentcdodds.workers.dev/svg');
|
|
70
|
+
url.searchParams.set('mermaid', compressed);
|
|
71
|
+
url.searchParams.set('theme', theme);
|
|
25
72
|
const timeout = AbortSignal.timeout(5000);
|
|
26
|
-
const svgResponse = await fetch(url, {
|
|
73
|
+
const svgResponse = await fetch(url, {
|
|
74
|
+
signal: timeout,
|
|
75
|
+
}).catch(() => null);
|
|
27
76
|
if (svgResponse?.ok) {
|
|
28
77
|
const svgText = await svgResponse.text();
|
|
29
78
|
if (svgText) {
|
|
79
|
+
const attributes = [
|
|
80
|
+
{
|
|
81
|
+
type: 'mdxJsxAttribute',
|
|
82
|
+
name: 'code',
|
|
83
|
+
value: node.value,
|
|
84
|
+
},
|
|
85
|
+
mdxStringExpressionAttribute('svg', svgText),
|
|
86
|
+
{
|
|
87
|
+
type: 'mdxJsxAttribute',
|
|
88
|
+
name: 'svgTheme',
|
|
89
|
+
value: theme,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
30
92
|
parent.children[index] = {
|
|
31
93
|
type: 'mdxJsxFlowElement',
|
|
32
|
-
name: '
|
|
33
|
-
attributes
|
|
34
|
-
{
|
|
35
|
-
type: 'mdxJsxAttribute',
|
|
36
|
-
name: 'className',
|
|
37
|
-
value: 'mermaid not-prose',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
type: 'mdxJsxAttribute',
|
|
41
|
-
name: 'dangerouslySetInnerHTML',
|
|
42
|
-
value: {
|
|
43
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
44
|
-
value: `{__html: ${JSON.stringify(svgText)}}`,
|
|
45
|
-
// This hack brought to you by this: https://github.com/syntax-tree/hast-util-to-estree/blob/e5ccb97e9f42bba90359ea6d0f83a11d74e0dad6/lib/handlers/mdx-expression.js#L35-L38
|
|
46
|
-
// no idea why we're required to have estree here, but I'm pretty sure someone is supposed to add it automatically for us and it just never happens...
|
|
47
|
-
data: {
|
|
48
|
-
estree: {
|
|
49
|
-
type: 'Program',
|
|
50
|
-
sourceType: 'script',
|
|
51
|
-
body: [
|
|
52
|
-
{
|
|
53
|
-
type: 'ExpressionStatement',
|
|
54
|
-
expression: {
|
|
55
|
-
type: 'ObjectExpression',
|
|
56
|
-
properties: [
|
|
57
|
-
{
|
|
58
|
-
type: 'Property',
|
|
59
|
-
method: false,
|
|
60
|
-
shorthand: false,
|
|
61
|
-
computed: false,
|
|
62
|
-
kind: 'init',
|
|
63
|
-
key: {
|
|
64
|
-
type: 'Identifier',
|
|
65
|
-
name: '__html',
|
|
66
|
-
},
|
|
67
|
-
value: {
|
|
68
|
-
type: 'Literal',
|
|
69
|
-
value: svgText,
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
],
|
|
94
|
+
name: 'Mermaid',
|
|
95
|
+
attributes,
|
|
81
96
|
children: [],
|
|
82
97
|
};
|
|
83
98
|
return;
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
}
|
|
102
|
+
const attributes = [
|
|
103
|
+
{
|
|
104
|
+
type: 'mdxJsxAttribute',
|
|
105
|
+
name: 'code',
|
|
106
|
+
value: node.value,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
87
109
|
parent.children[index] = {
|
|
88
110
|
type: 'mdxJsxFlowElement',
|
|
89
111
|
name: 'Mermaid',
|
|
90
|
-
attributes
|
|
91
|
-
{
|
|
92
|
-
type: 'mdxJsxAttribute',
|
|
93
|
-
name: 'code',
|
|
94
|
-
value: node.value,
|
|
95
|
-
},
|
|
96
|
-
],
|
|
112
|
+
attributes,
|
|
97
113
|
children: [],
|
|
98
114
|
};
|
|
99
115
|
})();
|
|
@@ -147,13 +163,14 @@ const rehypePlugins = [
|
|
|
147
163
|
];
|
|
148
164
|
const verboseLog = process.env.EPICSHOP_VERBOSE_LOG === 'true' ? console.log : () => { };
|
|
149
165
|
export async function compileMdx(file, { request, timings, forceFresh, } = {}) {
|
|
166
|
+
const mermaidTheme = getMermaidTheme(request);
|
|
150
167
|
const stat = await fs.promises
|
|
151
168
|
.stat(file)
|
|
152
169
|
.catch((error) => ({ error }));
|
|
153
170
|
if ('error' in stat) {
|
|
154
171
|
throw new Error(`File stat cannot be read: ${stat.error}`);
|
|
155
172
|
}
|
|
156
|
-
const key = `file:${file}`;
|
|
173
|
+
const key = `file:${file}:mermaid:${mermaidTheme}`;
|
|
157
174
|
forceFresh = await shouldForceFresh({ forceFresh, request, key });
|
|
158
175
|
const existingCacheEntry = await compiledInstructionMarkdownCache.get(key);
|
|
159
176
|
if (!forceFresh && existingCacheEntry) {
|
|
@@ -165,10 +182,10 @@ export async function compileMdx(file, { request, timings, forceFresh, } = {}) {
|
|
|
165
182
|
request,
|
|
166
183
|
timings,
|
|
167
184
|
forceFresh,
|
|
168
|
-
getFreshValue: () => compileMdxImpl(file),
|
|
185
|
+
getFreshValue: () => compileMdxImpl(file, { mermaidTheme }),
|
|
169
186
|
});
|
|
170
187
|
}
|
|
171
|
-
async function compileMdxImpl(file) {
|
|
188
|
+
async function compileMdxImpl(file, { mermaidTheme }) {
|
|
172
189
|
let title = null;
|
|
173
190
|
const epicVideoEmbeds = [];
|
|
174
191
|
try {
|
|
@@ -180,7 +197,7 @@ async function compileMdxImpl(file) {
|
|
|
180
197
|
options.remarkPlugins = [
|
|
181
198
|
...(options.remarkPlugins ?? []),
|
|
182
199
|
gfm,
|
|
183
|
-
remarkMermaidCodeToSvg,
|
|
200
|
+
[remarkMermaidCodeToSvg, { theme: mermaidTheme }],
|
|
184
201
|
() => (tree) => {
|
|
185
202
|
visit(tree, 'heading', (node) => {
|
|
186
203
|
if (title)
|
package/dist/db.server.d.ts
CHANGED
|
@@ -113,6 +113,13 @@ declare const DataSchema: z.ZodObject<{
|
|
|
113
113
|
defaultView?: string | undefined;
|
|
114
114
|
activeSidebarTab?: number | undefined;
|
|
115
115
|
}>>>;
|
|
116
|
+
offlineVideo: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
117
|
+
downloadResolution: z.ZodOptional<z.ZodEnum<["best", "high", "medium", "low"]>>;
|
|
118
|
+
}, "strip", z.ZodTypeAny, {
|
|
119
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
120
|
+
}, {
|
|
121
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
122
|
+
}>>>;
|
|
116
123
|
presence: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
117
124
|
optOut: z.ZodBoolean;
|
|
118
125
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -152,6 +159,9 @@ declare const DataSchema: z.ZodObject<{
|
|
|
152
159
|
defaultView?: string | undefined;
|
|
153
160
|
activeSidebarTab?: number | undefined;
|
|
154
161
|
};
|
|
162
|
+
offlineVideo: {
|
|
163
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
164
|
+
};
|
|
155
165
|
presence: {
|
|
156
166
|
optOut: boolean;
|
|
157
167
|
};
|
|
@@ -179,6 +189,9 @@ declare const DataSchema: z.ZodObject<{
|
|
|
179
189
|
defaultView?: string | undefined;
|
|
180
190
|
activeSidebarTab?: number | undefined;
|
|
181
191
|
} | undefined;
|
|
192
|
+
offlineVideo?: {
|
|
193
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
194
|
+
} | undefined;
|
|
182
195
|
presence?: {
|
|
183
196
|
optOut: boolean;
|
|
184
197
|
} | undefined;
|
|
@@ -282,6 +295,9 @@ declare const DataSchema: z.ZodObject<{
|
|
|
282
295
|
defaultView?: string | undefined;
|
|
283
296
|
activeSidebarTab?: number | undefined;
|
|
284
297
|
};
|
|
298
|
+
offlineVideo: {
|
|
299
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
300
|
+
};
|
|
285
301
|
presence: {
|
|
286
302
|
optOut: boolean;
|
|
287
303
|
};
|
|
@@ -333,6 +349,9 @@ declare const DataSchema: z.ZodObject<{
|
|
|
333
349
|
defaultView?: string | undefined;
|
|
334
350
|
activeSidebarTab?: number | undefined;
|
|
335
351
|
} | undefined;
|
|
352
|
+
offlineVideo?: {
|
|
353
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
354
|
+
} | undefined;
|
|
336
355
|
presence?: {
|
|
337
356
|
optOut: boolean;
|
|
338
357
|
} | undefined;
|
|
@@ -390,6 +409,9 @@ export declare function readDb(): Promise<{
|
|
|
390
409
|
defaultView?: string | undefined;
|
|
391
410
|
activeSidebarTab?: number | undefined;
|
|
392
411
|
};
|
|
412
|
+
offlineVideo: {
|
|
413
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
414
|
+
};
|
|
393
415
|
presence: {
|
|
394
416
|
optOut: boolean;
|
|
395
417
|
};
|
|
@@ -487,6 +509,9 @@ export declare function getPreferences(): Promise<{
|
|
|
487
509
|
defaultView?: string | undefined;
|
|
488
510
|
activeSidebarTab?: number | undefined;
|
|
489
511
|
};
|
|
512
|
+
offlineVideo: {
|
|
513
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
514
|
+
};
|
|
490
515
|
presence: {
|
|
491
516
|
optOut: boolean;
|
|
492
517
|
};
|
|
@@ -515,6 +540,9 @@ export declare function setPreferences(preferences: z.input<typeof DataSchema>['
|
|
|
515
540
|
defaultView?: string | undefined;
|
|
516
541
|
activeSidebarTab?: number | undefined;
|
|
517
542
|
};
|
|
543
|
+
offlineVideo: {
|
|
544
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
545
|
+
};
|
|
518
546
|
presence: {
|
|
519
547
|
optOut?: boolean | undefined;
|
|
520
548
|
};
|
|
@@ -549,6 +577,9 @@ export declare function markOnboardingComplete(featureId: string): Promise<{
|
|
|
549
577
|
defaultView?: string | undefined;
|
|
550
578
|
activeSidebarTab?: number | undefined;
|
|
551
579
|
} | undefined;
|
|
580
|
+
offlineVideo?: {
|
|
581
|
+
downloadResolution?: "best" | "high" | "medium" | "low" | undefined;
|
|
582
|
+
} | undefined;
|
|
552
583
|
presence?: {
|
|
553
584
|
optOut: boolean;
|
|
554
585
|
} | undefined;
|
package/dist/db.server.js
CHANGED
|
@@ -38,6 +38,13 @@ export const PlayerPreferencesSchema = z
|
|
|
38
38
|
})
|
|
39
39
|
.optional()
|
|
40
40
|
.default({});
|
|
41
|
+
const OfflineVideoResolutionSchema = z.enum(['best', 'high', 'medium', 'low']);
|
|
42
|
+
const OfflineVideoPreferencesSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
downloadResolution: OfflineVideoResolutionSchema.optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional()
|
|
47
|
+
.default({});
|
|
41
48
|
const PresencePreferencesSchema = z
|
|
42
49
|
.object({
|
|
43
50
|
optOut: z.boolean(),
|
|
@@ -55,6 +62,7 @@ const DataSchema = z.object({
|
|
|
55
62
|
preferences: z
|
|
56
63
|
.object({
|
|
57
64
|
player: PlayerPreferencesSchema,
|
|
65
|
+
offlineVideo: OfflineVideoPreferencesSchema,
|
|
58
66
|
presence: PresencePreferencesSchema,
|
|
59
67
|
playground: z
|
|
60
68
|
.object({
|
|
@@ -267,6 +275,10 @@ export async function setPreferences(preferences) {
|
|
|
267
275
|
...data?.preferences?.player,
|
|
268
276
|
...preferences?.player,
|
|
269
277
|
},
|
|
278
|
+
offlineVideo: {
|
|
279
|
+
...data?.preferences?.offlineVideo,
|
|
280
|
+
...preferences?.offlineVideo,
|
|
281
|
+
},
|
|
270
282
|
presence: {
|
|
271
283
|
...data?.preferences?.presence,
|
|
272
284
|
...preferences?.presence,
|
|
@@ -6,11 +6,21 @@ import { pipeline } from 'node:stream/promises';
|
|
|
6
6
|
import { getApps, getExercises, getWorkshopFinished, getWorkshopInstructions, } from "./apps.server.js";
|
|
7
7
|
import { getWorkshopConfig } from "./config.server.js";
|
|
8
8
|
import { resolvePrimaryDir } from "./data-storage.server.js";
|
|
9
|
-
import { getAuthInfo, getClientId } from "./db.server.js";
|
|
9
|
+
import { getAuthInfo, getClientId, getPreferences } from "./db.server.js";
|
|
10
10
|
import { getEpicVideoInfos } from "./epic-api.server.js";
|
|
11
11
|
import { getEnv } from "./init-env.js";
|
|
12
12
|
import { logger } from "./logger.js";
|
|
13
13
|
import { OFFLINE_VIDEO_CRYPTO_VERSION, createOfflineVideoCipher, createOfflineVideoDecipher, createOfflineVideoIv, createOfflineVideoSalt, decodeOfflineVideoIv, deriveOfflineVideoKey, encodeOfflineVideoIv, getCryptoRange, incrementIv, } from "./offline-video-crypto.server.js";
|
|
14
|
+
const offlineVideoDownloadResolutions = [
|
|
15
|
+
'best',
|
|
16
|
+
'high',
|
|
17
|
+
'medium',
|
|
18
|
+
'low',
|
|
19
|
+
];
|
|
20
|
+
function isOfflineVideoDownloadResolution(value) {
|
|
21
|
+
return (typeof value === 'string' &&
|
|
22
|
+
offlineVideoDownloadResolutions.includes(value));
|
|
23
|
+
}
|
|
14
24
|
const log = logger('epic:offline-videos');
|
|
15
25
|
const offlineVideoDirectoryName = 'offline-videos';
|
|
16
26
|
const offlineVideoIndexFileName = 'index.json';
|
|
@@ -275,13 +285,29 @@ async function getWorkshopVideoCollection({ request, } = {}) {
|
|
|
275
285
|
}
|
|
276
286
|
return { videos, totalEmbeds: embedUrls.size, unavailable };
|
|
277
287
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
288
|
+
async function getOfflineVideoDownloadResolution() {
|
|
289
|
+
const preferences = await getPreferences();
|
|
290
|
+
const resolution = preferences?.offlineVideo?.downloadResolution;
|
|
291
|
+
if (isOfflineVideoDownloadResolution(resolution)) {
|
|
292
|
+
return resolution;
|
|
293
|
+
}
|
|
294
|
+
return 'best';
|
|
295
|
+
}
|
|
296
|
+
const muxMp4Variants = {
|
|
297
|
+
high: (playbackId) => `https://stream.mux.com/${playbackId}/high.mp4`,
|
|
298
|
+
medium: (playbackId) => `https://stream.mux.com/${playbackId}/medium.mp4`,
|
|
299
|
+
low: (playbackId) => `https://stream.mux.com/${playbackId}/low.mp4`,
|
|
300
|
+
source: (playbackId) => `https://stream.mux.com/${playbackId}.mp4`,
|
|
301
|
+
};
|
|
302
|
+
const muxResolutionOrder = {
|
|
303
|
+
best: ['source', 'high', 'medium', 'low'],
|
|
304
|
+
high: ['high', 'medium', 'low', 'source'],
|
|
305
|
+
medium: ['medium', 'low', 'high', 'source'],
|
|
306
|
+
low: ['low', 'medium', 'high', 'source'],
|
|
307
|
+
};
|
|
308
|
+
function getMuxMp4Urls(playbackId, resolution) {
|
|
309
|
+
const order = muxResolutionOrder[resolution] ?? muxResolutionOrder.best;
|
|
310
|
+
return order.map((variant) => muxMp4Variants[variant](playbackId));
|
|
285
311
|
}
|
|
286
312
|
async function isOfflineVideoReady(index, playbackId, keyId, cryptoVersion, workshop) {
|
|
287
313
|
const entry = index[playbackId];
|
|
@@ -302,8 +328,8 @@ async function isOfflineVideoReady(index, playbackId, keyId, cryptoVersion, work
|
|
|
302
328
|
return false;
|
|
303
329
|
}
|
|
304
330
|
}
|
|
305
|
-
async function downloadMuxVideo({ playbackId, filePath, key, iv, }) {
|
|
306
|
-
const urls = getMuxMp4Urls(playbackId);
|
|
331
|
+
async function downloadMuxVideo({ playbackId, filePath, key, iv, resolution, }) {
|
|
332
|
+
const urls = getMuxMp4Urls(playbackId, resolution);
|
|
307
333
|
let lastError = null;
|
|
308
334
|
for (const url of urls) {
|
|
309
335
|
const response = await fetch(url).catch((error) => {
|
|
@@ -328,7 +354,7 @@ async function downloadMuxVideo({ playbackId, filePath, key, iv, }) {
|
|
|
328
354
|
}
|
|
329
355
|
throw lastError ?? new Error(`Unable to download video ${playbackId}`);
|
|
330
356
|
}
|
|
331
|
-
async function runOfflineVideoDownloads({ videos, index, keyInfo, workshop, }) {
|
|
357
|
+
async function runOfflineVideoDownloads({ videos, index, keyInfo, workshop, resolution, }) {
|
|
332
358
|
for (const video of videos) {
|
|
333
359
|
const updatedAt = new Date().toISOString();
|
|
334
360
|
downloadState.current = {
|
|
@@ -357,6 +383,7 @@ async function runOfflineVideoDownloads({ videos, index, keyInfo, workshop, }) {
|
|
|
357
383
|
filePath: path.join(getOfflineVideoDir(), entry.fileName),
|
|
358
384
|
key: keyInfo.key,
|
|
359
385
|
iv,
|
|
386
|
+
resolution,
|
|
360
387
|
});
|
|
361
388
|
index[video.playbackId] = {
|
|
362
389
|
...entry,
|
|
@@ -505,11 +532,13 @@ export async function startOfflineVideoDownload({ request, } = {}) {
|
|
|
505
532
|
errors: [],
|
|
506
533
|
};
|
|
507
534
|
if (downloads.length > 0) {
|
|
535
|
+
const resolution = await getOfflineVideoDownloadResolution();
|
|
508
536
|
void runOfflineVideoDownloads({
|
|
509
537
|
videos: downloads,
|
|
510
538
|
index,
|
|
511
539
|
keyInfo,
|
|
512
540
|
workshop,
|
|
541
|
+
resolution,
|
|
513
542
|
}).catch((error) => {
|
|
514
543
|
log.error('Offline video downloads failed', error);
|
|
515
544
|
downloadState.status = 'error';
|
|
@@ -534,6 +563,7 @@ export async function downloadOfflineVideo({ playbackId, title, url, }) {
|
|
|
534
563
|
});
|
|
535
564
|
if (!keyInfo)
|
|
536
565
|
return { status: 'error' };
|
|
566
|
+
const resolution = await getOfflineVideoDownloadResolution();
|
|
537
567
|
const existing = index[playbackId];
|
|
538
568
|
if (existing?.status === 'ready' &&
|
|
539
569
|
existing.keyId === keyInfo.keyId &&
|
|
@@ -567,6 +597,7 @@ export async function downloadOfflineVideo({ playbackId, title, url, }) {
|
|
|
567
597
|
filePath: path.join(getOfflineVideoDir(), entry.fileName),
|
|
568
598
|
key: keyInfo.key,
|
|
569
599
|
iv,
|
|
600
|
+
resolution,
|
|
570
601
|
});
|
|
571
602
|
index[playbackId] = {
|
|
572
603
|
...entry,
|