Skip to content

Commit d6247d0

Browse files
committed
feat: Implement instance fallback after tunnel failures & error message
merge
1 parent 7f34423 commit d6247d0

1 file changed

Lines changed: 45 additions & 28 deletions

File tree

src/core/data/cobalt/index.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isDeepStrictEqual } from "node:util"
2+
13
import type { CobaltDownloadParams, SuccessfulCobaltMediaResponse } from "@/core/data/cobalt/download"
24
import { getDownloadLink } from "@/core/data/cobalt/download"
35
import type { ApiServer } from "@/core/data/cobalt/server"
@@ -6,34 +8,15 @@ import { retrieveTunneledMedia } from "@/core/data/cobalt/tunnel"
68

79
import type { Result } from "@/core/utils/result"
810
import { error, ok } from "@/core/utils/result"
9-
import type { CompoundText, Text } from "@/core/utils/text"
11+
import type { Text } from "@/core/utils/text"
1012
import { compound, literal, translatable } from "@/core/utils/text"
1113
import { safeUrlSchema } from "@/core/utils/url"
1214

1315
export { type CobaltDownloadParams } from "@/core/data/cobalt/download"
1416
export { type ApiServer, apiServerSchema } from "@/core/data/cobalt/server"
1517

1618
export async function download(params: CobaltDownloadParams, apiPool: ApiServer[]): Promise<Result<DownloadedMedia[], Text>> {
17-
const link = await tryGetDownloadLink(params, apiPool)
18-
if (!link.success)
19-
return link
20-
const resolvedMedia = resolveMedia(link.result, params.downloadMode === "audio")
21-
const downloadedMedia = await Promise.all(
22-
resolvedMedia.map(m => downloadResolvedMedia(m, link.result.api)),
23-
)
24-
25-
const successfulDownloads = downloadedMedia
26-
.filter(m => m.success)
27-
.map(m => m.result)
28-
const failedDownloads = downloadedMedia
29-
.filter(m => !m.success)
30-
.map(m => m.error)
31-
if (successfulDownloads.length === 0) {
32-
if (failedDownloads.length === 1)
33-
return error(compound(literal(`${link.result.api.name}: `), failedDownloads[0]))
34-
return error(compound(...failedDownloads.map(m => compound(literal(`\n${link.result.api.name}: `), m))))
35-
}
36-
return ok(successfulDownloads)
19+
return await tryDownload(params, apiPool)
3720
}
3821

3922
export type DownloadedMedia = { file: DownloadedMediaContent, filename?: string }
@@ -50,7 +33,7 @@ async function downloadResolvedMedia(link: ResolvedMedia, api: ApiServer): Promi
5033
}
5134

5235
type ResolvedMedia = { url: string, filename?: string }
53-
function resolveMedia(link: DownloadLink, audio?: boolean): ResolvedMedia[] {
36+
function resolveMedia(link: SuccessfulCobaltMediaResponse, audio?: boolean): ResolvedMedia[] {
5437
if (link.status === "picker") {
5538
if (audio && link.audio) {
5639
const source = new URL(link.audio)
@@ -61,17 +44,35 @@ function resolveMedia(link: DownloadLink, audio?: boolean): ResolvedMedia[] {
6144
return [{ url: link.url, filename: link.filename }]
6245
}
6346

64-
type DownloadLink = SuccessfulCobaltMediaResponse & { api: ApiServer }
65-
async function tryGetDownloadLink(params: CobaltDownloadParams, apiPool: ApiServer[], fails: Text[] = []): Promise<Result<DownloadLink, CompoundText>> {
47+
type DownloadFailure = { reason: Text, api: ApiServer }
48+
function formatDownloadFailures(fails: DownloadFailure[]): Text {
49+
const mergedFails = fails.reduce((acc, fail) => {
50+
const existing = acc.find(f => isDeepStrictEqual(f.reason, fail.reason))
51+
if (existing) {
52+
existing.apis.push(fail.api)
53+
} else {
54+
acc.push({ reason: fail.reason, apis: [fail.api] })
55+
}
56+
return acc
57+
}, [] as { reason: Text, apis: ApiServer[] }[])
58+
return compound(...mergedFails.map(f => compound(
59+
literal("\n"),
60+
...f.apis.map((api, i) => literal((i === 0 ? "" : ", ") + api.name)),
61+
literal(": "),
62+
f.reason,
63+
)))
64+
}
65+
66+
async function tryDownload(params: CobaltDownloadParams, apiPool: ApiServer[], fails: DownloadFailure[] = []): Promise<Result<DownloadedMedia[], Text>> {
6667
const currentApi = apiPool.at(0)
6768
if (!currentApi)
68-
return error(compound(...fails))
69+
return error(formatDownloadFailures(fails))
6970

7071
const next = (reason: Text) =>
71-
tryGetDownloadLink(
72+
tryDownload(
7273
params,
7374
apiPool.slice(1),
74-
[...fails, compound(literal(`\n${currentApi.name}: `), reason)],
75+
[...fails, { reason, api: currentApi }],
7576
)
7677

7778
if (currentApi.unsafe && !(await safeUrlSchema.safeParseAsync(currentApi.url)).success)
@@ -82,5 +83,21 @@ async function tryGetDownloadLink(params: CobaltDownloadParams, apiPool: ApiServ
8283
if (!res.success)
8384
return next(res.error)
8485

85-
return { success: res.success, result: { ...res.result, api: currentApi } }
86+
const resolvedMedia = resolveMedia(res.result, params.downloadMode === "audio")
87+
const downloadedMedia = await Promise.all(
88+
resolvedMedia.map(m => downloadResolvedMedia(m, currentApi)),
89+
)
90+
91+
const successfulDownloads = downloadedMedia
92+
.filter(m => m.success)
93+
.map(m => m.result)
94+
const failedDownloads = downloadedMedia
95+
.filter(m => !m.success)
96+
.map(m => m.error)
97+
if (successfulDownloads.length === 0) {
98+
if (failedDownloads.length === 1)
99+
return next(failedDownloads[0])
100+
return next(compound(...failedDownloads))
101+
}
102+
return ok(successfulDownloads)
86103
}

0 commit comments

Comments
 (0)