Skip to content

Commit 1a492e6

Browse files
committed
fix(player): reuse legacy audio mp4 downloads
1 parent 932627f commit 1a492e6

5 files changed

Lines changed: 115 additions & 27 deletions

File tree

‎src/downloadEpisode.ts‎

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import getUrlExtension from "./utility/getUrlExtension";
1414
import getExtensionFromContentType from "./utility/getExtensionFromContentType";
1515
import {
1616
getEpisodeMediaType,
17+
getEpisodeMediaTypeWithAudioContainerHint,
1718
getMediaTypeFromContentType,
1819
getMediaTypeFromExtension,
1920
getMediaTypeFromPath,
21+
isAudioContainerExtension,
2022
isPlayableMediaExtension,
2123
isSameMediaSource,
2224
} from "./utility/mediaType";
@@ -357,10 +359,7 @@ function isExplicitAudioContainer(
357359
extension: string | null | undefined,
358360
mediaTypeHint?: EpisodeMediaType,
359361
): boolean {
360-
if (mediaTypeHint !== "audio" || !extension) return false;
361-
362-
const normalizedExtension = extension.toLowerCase();
363-
return normalizedExtension === "mp4" || normalizedExtension === "webm";
362+
return mediaTypeHint === "audio" && isAudioContainerExtension(extension);
364363
}
365364

366365
function normalizeAudioExtension(
@@ -377,26 +376,6 @@ function normalizeAudioExtension(
377376
return extension;
378377
}
379378

380-
function getRegisteredEpisodeMediaType(
381-
episode: Episode & { filePath?: string },
382-
currentEpisodeMediaTypeHint?: EpisodeMediaType,
383-
): EpisodeMediaType {
384-
if (episode.mediaType) return episode.mediaType;
385-
const fileExtension = episode.filePath
386-
? getUrlExtension(episode.filePath)
387-
: null;
388-
if (
389-
isExplicitAudioContainer(
390-
fileExtension,
391-
currentEpisodeMediaTypeHint,
392-
)
393-
) {
394-
return "audio";
395-
}
396-
397-
return getEpisodeMediaType(episode);
398-
}
399-
400379
/**
401380
* UNUSED IN PRODUCTION — kept only for the #178 tests. Do NOT use this for
402381
* transcription: it derives the on-disk path from the download-path template and
@@ -527,7 +506,7 @@ export async function getEpisodeAudioBuffer(
527506
registered?.filePath &&
528507
isSameMediaSource(registered.streamUrl, episode.streamUrl)
529508
) {
530-
const registeredMediaType = getRegisteredEpisodeMediaType(
509+
const registeredMediaType = getEpisodeMediaTypeWithAudioContainerHint(
531510
registered,
532511
episode.mediaType === "audio" ? episodeMediaType : undefined,
533512
);

‎src/ui/PodcastView/EpisodePlayer.svelte‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
} from "src/utility/playbackRate";
4141
import {
4242
getEpisodeMediaType,
43+
getEpisodeMediaTypeWithAudioContainerHint,
4344
isSameMediaSource,
4445
} from "src/utility/mediaType";
4546
import type DownloadedEpisode from "src/types/DownloadedEpisode";
@@ -392,7 +393,7 @@
392393
if (shouldUseDownloadedEpisode(episode, downloadedEpisode)) {
393394
return {
394395
src: createMediaUrlObjectFromFilePath(downloadedEpisode.filePath),
395-
mediaType: getEpisodeMediaType(downloadedEpisode),
396+
mediaType: getDownloadedEpisodeMediaType(episode, downloadedEpisode),
396397
};
397398
}
398399
@@ -417,7 +418,10 @@
417418
): downloadedEpisode is DownloadedEpisode {
418419
if (!downloadedEpisode?.filePath) return false;
419420
420-
const downloadedMediaType = getEpisodeMediaType(downloadedEpisode);
421+
const downloadedMediaType = getDownloadedEpisodeMediaType(
422+
episode,
423+
downloadedEpisode,
424+
);
421425
if (episode.mediaType && downloadedMediaType !== episode.mediaType) {
422426
return false;
423427
}
@@ -430,6 +434,16 @@
430434
return isSameMediaSource(downloadedEpisode.streamUrl, episode.streamUrl);
431435
}
432436
437+
function getDownloadedEpisodeMediaType(
438+
episode: Episode,
439+
downloadedEpisode: DownloadedEpisode,
440+
): EpisodeMediaType {
441+
return getEpisodeMediaTypeWithAudioContainerHint(
442+
downloadedEpisode,
443+
episode.mediaType === "audio" ? "audio" : undefined,
444+
);
445+
}
446+
433447
$: if (mediaElement && mediaElement.playbackRate !== $playbackRate) {
434448
mediaElement.playbackRate = $playbackRate;
435449
}

‎src/ui/PodcastView/EpisodePlayer.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,39 @@ describe("EpisodePlayer", () => {
336336
expect(video.getAttribute("src")).toBe(videoEpisode.streamUrl);
337337
});
338338

339+
test("uses a legacy downloaded audio mp4 file for an explicitly audio feed episode", async () => {
340+
mockVaultFile("Downloads/legacy-audio.mp4");
341+
const audioEpisode: Episode = {
342+
...testEpisode,
343+
title: "Legacy Audio MP4",
344+
streamUrl: "https://cdn.example.com/episode.mp4",
345+
mediaType: "audio",
346+
};
347+
downloadedEpisodes.set({
348+
[testEpisode.podcastName]: [
349+
{
350+
...testEpisode,
351+
title: "Legacy Audio MP4",
352+
streamUrl: "https://cdn.example.com/episode.mp4",
353+
filePath: "Downloads/legacy-audio.mp4",
354+
size: 10,
355+
},
356+
],
357+
});
358+
currentEpisode.set(audioEpisode);
359+
360+
const { container } = render(EpisodePlayer);
361+
await waitFor(() => {
362+
expect(container.querySelector("audio")).not.toBeNull();
363+
});
364+
const audio = container.querySelector("audio") as HTMLAudioElement;
365+
366+
expect(container.querySelector("video")).toBeNull();
367+
expect(audio.getAttribute("src")).toBe(
368+
"app://resource/Downloads/legacy-audio.mp4?token",
369+
);
370+
});
371+
339372
test("uses a matching downloaded video file to classify old extensionless records", async () => {
340373
mockVaultFile("Downloads/video.mp4");
341374
const extensionlessEpisode: Episode = {

‎src/utility/mediaType.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Episode } from "src/types/Episode";
33
import type { LocalEpisode } from "src/types/LocalEpisode";
44
import {
55
getEpisodeMediaType,
6+
getEpisodeMediaTypeWithAudioContainerHint,
67
getMediaTypeFromContentType,
78
isSameMediaSource,
89
} from "./mediaType";
@@ -87,6 +88,40 @@ describe("mediaType", () => {
8788
expect(getEpisodeMediaType(episode)).toBe("audio");
8889
});
8990

91+
test("uses an audio hint for legacy ambiguous container file paths", () => {
92+
const episode = {
93+
title: "Legacy Downloaded Audio MP4",
94+
streamUrl: "https://example.com/episode.mp4",
95+
url: "https://example.com/episode",
96+
description: "",
97+
content: "",
98+
podcastName: "Feed",
99+
filePath: "Podcasts/downloaded-audio.mp4",
100+
} satisfies Episode & { filePath: string };
101+
102+
expect(getEpisodeMediaType(episode)).toBe("video");
103+
expect(getEpisodeMediaTypeWithAudioContainerHint(episode, "audio")).toBe(
104+
"audio",
105+
);
106+
});
107+
108+
test("does not let an audio hint override explicit video metadata", () => {
109+
const episode = {
110+
title: "Downloaded Video MP4",
111+
streamUrl: "https://example.com/episode.mp4",
112+
url: "https://example.com/episode",
113+
description: "",
114+
content: "",
115+
podcastName: "Feed",
116+
mediaType: "video",
117+
filePath: "Podcasts/downloaded-video.mp4",
118+
} satisfies Episode & { filePath: string };
119+
120+
expect(getEpisodeMediaTypeWithAudioContainerHint(episode, "audio")).toBe(
121+
"video",
122+
);
123+
});
124+
90125
test("trusts remote episode media metadata before URL extension fallback", () => {
91126
expect(
92127
getEpisodeMediaType({

‎src/utility/mediaType.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,33 @@ export function getEpisodeMediaType(episode: Episode): EpisodeMediaType {
7272
return getMediaTypeFromPath(episode.streamUrl) ?? "audio";
7373
}
7474

75+
export function isAudioContainerExtension(
76+
extension?: string | null,
77+
): boolean {
78+
if (!extension) return false;
79+
80+
const normalizedExtension = extension.toLowerCase();
81+
return normalizedExtension === "mp4" || normalizedExtension === "webm";
82+
}
83+
84+
export function getEpisodeMediaTypeWithAudioContainerHint(
85+
episode: Episode,
86+
mediaTypeHint?: EpisodeMediaType,
87+
): EpisodeMediaType {
88+
if (episode.mediaType) return episode.mediaType;
89+
90+
const filePath = (episode as Partial<LocalEpisode>).filePath;
91+
const fileExtension = filePath ? getUrlExtension(filePath) : null;
92+
if (
93+
mediaTypeHint === "audio" &&
94+
isAudioContainerExtension(fileExtension)
95+
) {
96+
return "audio";
97+
}
98+
99+
return getEpisodeMediaType(episode);
100+
}
101+
75102
export function isSameMediaSource(a: string, b: string): boolean {
76103
if (a === b) return true;
77104
if (!a || !b) return false;

0 commit comments

Comments
 (0)