@epic-web/workshop-utils 6.55.0 → 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/epic-api.server.js +14 -7
- package/dist/offline-videos.server.d.ts +1 -0
- package/dist/offline-videos.server.js +45 -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,
|
package/dist/epic-api.server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { invariant } from '@epic-web/invariant';
|
|
2
2
|
import * as cookie from 'cookie';
|
|
3
3
|
import md5 from 'md5-hex';
|
|
4
|
+
import PQueue from 'p-queue';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { getExerciseApp, getExercises, getWorkshopFinished, getWorkshopInstructions, } from "./apps.server.js";
|
|
6
7
|
import { cachified, epicApiCache } from "./cache.server.js";
|
|
@@ -84,6 +85,7 @@ const CachedEpicVideoInfoSchema = z
|
|
|
84
85
|
}))
|
|
85
86
|
.or(z.null());
|
|
86
87
|
const videoInfoLog = log.logger('video-info');
|
|
88
|
+
const EPIC_VIDEO_INFO_CONCURRENCY = 6;
|
|
87
89
|
export async function getEpicVideoInfos(epicWebUrls, { request, timings } = {}) {
|
|
88
90
|
if (!epicWebUrls) {
|
|
89
91
|
videoInfoLog.warn('no epic web URLs provided, returning empty object');
|
|
@@ -92,18 +94,23 @@ export async function getEpicVideoInfos(epicWebUrls, { request, timings } = {})
|
|
|
92
94
|
const authInfo = await getAuthInfo();
|
|
93
95
|
if (getEnv().EPICSHOP_DEPLOYED)
|
|
94
96
|
return {};
|
|
95
|
-
|
|
97
|
+
const uniqueUrls = Array.from(new Set(epicWebUrls));
|
|
98
|
+
videoInfoLog(`fetching epic video infos for ${uniqueUrls.length} URLs`);
|
|
96
99
|
const epicVideoInfos = {};
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
const queue = new PQueue({ concurrency: EPIC_VIDEO_INFO_CONCURRENCY });
|
|
101
|
+
const results = await Promise.all(uniqueUrls.map((epicVideoEmbed) => queue.add(async () => ({
|
|
102
|
+
epicVideoEmbed,
|
|
103
|
+
info: await getEpicVideoInfo({
|
|
99
104
|
epicVideoEmbed,
|
|
100
105
|
accessToken: authInfo?.tokenSet.access_token,
|
|
101
106
|
request,
|
|
102
107
|
timings,
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
}),
|
|
109
|
+
}))));
|
|
110
|
+
for (const result of results) {
|
|
111
|
+
if (!result.info)
|
|
112
|
+
continue;
|
|
113
|
+
epicVideoInfos[result.epicVideoEmbed] = result.info;
|
|
107
114
|
}
|
|
108
115
|
videoInfoLog(`successfully fetched ${Object.keys(epicVideoInfos).length} epic video infos`);
|
|
109
116
|
return epicVideoInfos;
|
|
@@ -62,6 +62,7 @@ export declare function getOfflineVideoDownloadState(): OfflineVideoDownloadStat
|
|
|
62
62
|
export declare function getOfflineVideoSummary({ request, }?: {
|
|
63
63
|
request?: Request;
|
|
64
64
|
}): Promise<OfflineVideoSummary>;
|
|
65
|
+
export declare function warmOfflineVideoSummary(): Promise<void>;
|
|
65
66
|
export declare function startOfflineVideoDownload({ request, }?: {
|
|
66
67
|
request?: Request;
|
|
67
68
|
}): Promise<OfflineVideoStartResult>;
|
|
@@ -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,
|
|
@@ -422,6 +449,9 @@ export async function getOfflineVideoSummary({ request, } = {}) {
|
|
|
422
449
|
downloadState,
|
|
423
450
|
};
|
|
424
451
|
}
|
|
452
|
+
export async function warmOfflineVideoSummary() {
|
|
453
|
+
await getWorkshopVideoCollection();
|
|
454
|
+
}
|
|
425
455
|
export async function startOfflineVideoDownload({ request, } = {}) {
|
|
426
456
|
if (getEnv().EPICSHOP_DEPLOYED) {
|
|
427
457
|
return {
|
|
@@ -502,11 +532,13 @@ export async function startOfflineVideoDownload({ request, } = {}) {
|
|
|
502
532
|
errors: [],
|
|
503
533
|
};
|
|
504
534
|
if (downloads.length > 0) {
|
|
535
|
+
const resolution = await getOfflineVideoDownloadResolution();
|
|
505
536
|
void runOfflineVideoDownloads({
|
|
506
537
|
videos: downloads,
|
|
507
538
|
index,
|
|
508
539
|
keyInfo,
|
|
509
540
|
workshop,
|
|
541
|
+
resolution,
|
|
510
542
|
}).catch((error) => {
|
|
511
543
|
log.error('Offline video downloads failed', error);
|
|
512
544
|
downloadState.status = 'error';
|
|
@@ -531,6 +563,7 @@ export async function downloadOfflineVideo({ playbackId, title, url, }) {
|
|
|
531
563
|
});
|
|
532
564
|
if (!keyInfo)
|
|
533
565
|
return { status: 'error' };
|
|
566
|
+
const resolution = await getOfflineVideoDownloadResolution();
|
|
534
567
|
const existing = index[playbackId];
|
|
535
568
|
if (existing?.status === 'ready' &&
|
|
536
569
|
existing.keyId === keyInfo.keyId &&
|
|
@@ -564,6 +597,7 @@ export async function downloadOfflineVideo({ playbackId, title, url, }) {
|
|
|
564
597
|
filePath: path.join(getOfflineVideoDir(), entry.fileName),
|
|
565
598
|
key: keyInfo.key,
|
|
566
599
|
iv,
|
|
600
|
+
resolution,
|
|
567
601
|
});
|
|
568
602
|
index[playbackId] = {
|
|
569
603
|
...entry,
|