Skip to content

Commit 4bb4cb7

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 6a179c1 + 3a5c34a commit 4bb4cb7

File tree

3 files changed

+78
-16
lines changed

3 files changed

+78
-16
lines changed

src/downloadEpisode.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Episode } from "./types/Episode";
55
import type { LocalEpisode } from "./types/LocalEpisode";
66
import { isLocalFile } from "./utility/isLocalFile";
77
import getUrlExtension from "./utility/getUrlExtension";
8+
import getExtensionFromContentType from "./utility/getExtensionFromContentType";
89

910
function getErrorMessage(error: unknown): string {
1011
return error instanceof Error ? error.message : String(error);
@@ -76,18 +77,11 @@ export default async function downloadEpisodeWithNotice(
7677
},
7778
});
7879

79-
const fileExtension = await detectAudioFileExtension(blob);
80-
if (!fileExtension) {
81-
update((bodyEl) => {
82-
bodyEl.createEl("p", {
83-
text: `Could not determine file extension for downloaded file. Blob: ${blob.size} bytes.`,
84-
});
85-
});
80+
const inferredExtension = await inferFileExtensionFromDownload(episode, blob);
81+
const normalizedType = (blob.type ?? "").toLowerCase();
82+
const typeAppearsAudio = normalizedType === "" || normalizedType.includes("audio");
8683

87-
throw new Error("Could not determine file extension");
88-
}
89-
90-
if (!blob.type.contains("audio") && !fileExtension) {
84+
if (!typeAppearsAudio && !inferredExtension) {
9185
update((bodyEl) => {
9286
bodyEl.createEl("p", {
9387
text: `Downloaded file is not an audio file. It is of type "${blob.type}". Blob: ${blob.size} bytes.`,
@@ -97,6 +91,8 @@ export default async function downloadEpisodeWithNotice(
9791
throw new Error("Not an audio file");
9892
}
9993

94+
const fileExtension = inferredExtension ?? "mp3";
95+
10096
try {
10197
update((bodyEl) => bodyEl.createEl("p", { text: "Creating file..." }));
10298

@@ -231,6 +227,23 @@ function getLocalFilePathFromLink(link: string): string | null {
231227
return null;
232228
}
233229

230+
async function inferFileExtensionFromDownload(
231+
episode: Episode,
232+
blob: Blob,
233+
): Promise<string | null> {
234+
const signatureExtension = await detectAudioFileExtension(blob);
235+
if (signatureExtension) {
236+
return signatureExtension;
237+
}
238+
239+
const urlExtension = getUrlExtension(episode.streamUrl);
240+
if (urlExtension) {
241+
return urlExtension;
242+
}
243+
244+
return getExtensionFromContentType(blob.type);
245+
}
246+
234247
export async function downloadEpisode(
235248
episode: Episode,
236249
downloadPathTemplate: string,
@@ -286,11 +299,10 @@ async function getFileExtension(url: string): Promise<string> {
286299
const response = await fetch(url, { method: "HEAD" });
287300
const contentType = response.headers.get("content-type");
288301

289-
if (contentType?.includes("audio/mpeg")) return "mp3";
290-
if (contentType?.includes("audio/mp4")) return "m4a";
291-
if (contentType?.includes("audio/ogg")) return "ogg";
292-
if (contentType?.includes("audio/wav")) return "wav";
293-
if (contentType?.includes("audio/x-m4a")) return "m4a";
302+
const extensionFromContentType = getExtensionFromContentType(contentType);
303+
if (extensionFromContentType) {
304+
return extensionFromContentType;
305+
}
294306

295307
// Default to mp3 if we can't determine the type
296308
return "mp3";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, test } from "vitest";
2+
import getExtensionFromContentType from "./getExtensionFromContentType";
3+
4+
describe("getExtensionFromContentType", () => {
5+
test("detects mp3 from standard mime type", () => {
6+
expect(getExtensionFromContentType("audio/mpeg")).toBe("mp3");
7+
});
8+
9+
test("handles mixed case mime types", () => {
10+
expect(getExtensionFromContentType("Audio/X-M4A")).toBe("m4a");
11+
});
12+
13+
test("returns null when mime type is not audio", () => {
14+
expect(getExtensionFromContentType("text/html")).toBeNull();
15+
});
16+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const CONTENT_TYPE_EXTENSION_MAP: Array<{
2+
pattern: RegExp;
3+
extension: string;
4+
}> = [
5+
{ pattern: /audio\/mpeg/i, extension: "mp3" },
6+
{ pattern: /audio\/mp3/i, extension: "mp3" },
7+
{ pattern: /audio\/mp4/i, extension: "m4a" },
8+
{ pattern: /audio\/x-m4a/i, extension: "m4a" },
9+
{ pattern: /audio\/aac/i, extension: "aac" },
10+
{ pattern: /audio\/ogg/i, extension: "ogg" },
11+
{ pattern: /audio\/wav/i, extension: "wav" },
12+
{ pattern: /audio\/x-wav/i, extension: "wav" },
13+
{ pattern: /audio\/flac/i, extension: "flac" },
14+
{ pattern: /audio\/x-flac/i, extension: "flac" },
15+
{ pattern: /audio\/x-ms-wma/i, extension: "wma" },
16+
{ pattern: /audio\/wma/i, extension: "wma" },
17+
{ pattern: /audio\/amr/i, extension: "amr" },
18+
];
19+
20+
export default function getExtensionFromContentType(
21+
contentType?: string | null,
22+
): string | null {
23+
if (!contentType) {
24+
return null;
25+
}
26+
27+
for (const { pattern, extension } of CONTENT_TYPE_EXTENSION_MAP) {
28+
if (pattern.test(contentType)) {
29+
return extension;
30+
}
31+
}
32+
33+
return null;
34+
}

0 commit comments

Comments
 (0)