Skip to content

Commit

Permalink
fix: fix downloads on mobile devices
Browse files Browse the repository at this point in the history
content-type header is case-insensitive on mobile

fix #69
  • Loading branch information
chhoumann committed Feb 27, 2023
1 parent 2bd0d36 commit 3acc595
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 73 deletions.
138 changes: 75 additions & 63 deletions src/downloadEpisode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { Notice, requestUrl } from "obsidian";
import { downloadedEpisodes } from "./store";
import { DownloadPathTemplateEngine } from "./TemplateEngine";
import { Episode } from "./types/Episode";
import Progressbar from "./ui/common/Progressbar.svelte";
import getUrlExtension from "./utility/getUrlExtension";

async function downloadFile(
url: string,
options?: Partial<{
onProgress: (progress: number, total: number) => void,
onFinished: () => void
onFinished: () => void;
onError: (error: Error) => void;
}>
) {
try {
Expand All @@ -24,78 +23,81 @@ async function downloadFile(
options?.onFinished?.();

return {
blob: new Blob([response.arrayBuffer], { type: response.headers['content-type'] ?? "" }),
blob: new Blob([response.arrayBuffer], {
type: response.headers["content-type"] ?? response.headers["Content-Type"] ?? "",
}),
contentLength,
receivedLength: contentLength,
responseUrl: url,
};
} catch (error) {
throw new Error(`Failed to download ${url}: ${error.message}`);
const err = new Error(`Failed to download ${url}:\n\n${error.message}`);
options?.onError?.(err);

throw err;
}
}

export default async function downloadEpisodeWithProgressNotice(episode: Episode, downloadPathTemplate: string): Promise<void> {
export default async function downloadEpisodeWithNotice(
episode: Episode,
downloadPathTemplate: string
): Promise<void> {
const { doc, update } = createNoticeDoc(`Download "${episode.title}"`);
const SOME_LARGE_INT_SO_THE_BOX_DOESNT_AUTO_CLOSE = 999999999;
const notice = new Notice(doc, SOME_LARGE_INT_SO_THE_BOX_DOESNT_AUTO_CLOSE);

update(bodyEl => bodyEl.createEl('p', { text: 'Starting download...' }));

let progressBar: Progressbar;
let percentEl: HTMLSpanElement;
update(bodyEl => {
percentEl = bodyEl.createSpan({ text: '0%' });
progressBar = new Progressbar({
target: bodyEl,
props: {
max: 100,
value: 0,
style: {
width: '100%',
height: '2rem',
}
}
});
update((bodyEl) => bodyEl.createEl("p", { text: "Starting download..." }));

update((bodyEl) => {
bodyEl.createEl("p", { text: "Downloading..." });
});

const { blob, responseUrl } = await downloadFile(episode.streamUrl, {
onProgress: (progress, total) => {
update(_ => {
percentEl.textContent = `${Math.floor(progress / total * 100)}%`;
progressBar.$set({ value: progress / total * 100 });
}, false);
},
onFinished: () => {
progressBar.$destroy();
update(bodyEl => bodyEl.createEl('p', { text: 'Download complete!' }));
update((bodyEl) =>
bodyEl.createEl("p", { text: "Download complete!" })
);
},
onError: (error) => {
update((bodyEl) =>
bodyEl.createEl("p", { text: `Download failed: ${error.message}` })
);
}
});

console.log(blob);
if (!blob.type.contains("audio")) {
update((bodyEl) => {
bodyEl.createEl("p", {
text: `Downloaded file is not an audio file. It is of type "${blob.type}". Blob: ${blob.size} bytes.`,
});
});

if (!blob.type.contains('audio')) {
throw new Error('Not an audio file');
throw new Error("Not an audio file");
}

try {
update(bodyEl => bodyEl.createEl('p', { text: `Creating file...` }));
update((bodyEl) => bodyEl.createEl("p", { text: `Creating file...` }));

await createEpisodeFile({
episode,
downloadPathTemplate,
blob,
responseUrl,
})
});

update(bodyEl => bodyEl.createEl('p', { text: `Successfully downloaded "${episode.title}" from ${episode.podcastName}.` }));
update((bodyEl) =>
bodyEl.createEl("p", {
text: `Successfully downloaded "${episode.title}" from ${episode.podcastName}.`,
})
);
} catch (error) {
update(bodyEl => {
bodyEl.createEl('p', {
text: `Failed to create file for downloaded episode "${episode.title}" from ${episode.podcastName}.`
update((bodyEl) => {
bodyEl.createEl("p", {
text: `Failed to create file for downloaded episode "${episode.title}" from ${episode.podcastName}.`,
});

const errorMsgEl = bodyEl.createEl('p', { text: error.message });
errorMsgEl.style.fontStyle = 'italic';
const errorMsgEl = bodyEl.createEl("p", { text: error.message });
errorMsgEl.style.fontStyle = "italic";
});
}

Expand All @@ -106,39 +108,44 @@ function createNoticeDoc(title: string) {
const doc = new DocumentFragment();
const container = doc.createDiv();
container.style.width = "100%";
container.style.display = 'flex';
container.style.display = "flex";

const titleEl = container.createEl('span', { text: title });
titleEl.style.textAlign = 'center';
titleEl.style.fontWeight = 'bold';
titleEl.style.marginBottom = '0.5em';
const titleEl = container.createEl("span", { text: title });
titleEl.style.textAlign = "center";
titleEl.style.fontWeight = "bold";
titleEl.style.marginBottom = "0.5em";

const bodyEl = doc.createDiv();
bodyEl.style.display = 'flex';
bodyEl.style.flexDirection = 'column';
bodyEl.style.alignItems = 'center';
bodyEl.style.justifyContent = 'center';
bodyEl.style.display = "flex";
bodyEl.style.flexDirection = "column";
bodyEl.style.alignItems = "center";
bodyEl.style.justifyContent = "center";

return {
doc,
update: (updateFn: (bodyEl: HTMLDivElement) => void, empty = true) => {
if (empty) bodyEl.empty();
updateFn(bodyEl);
},
}
};
}

async function createEpisodeFile({episode, downloadPathTemplate, blob, responseUrl }: {
episode: Episode,
downloadPathTemplate: string,
blob: Blob,
responseUrl: string
async function createEpisodeFile({
episode,
downloadPathTemplate,
blob,
responseUrl,
}: {
episode: Episode;
downloadPathTemplate: string;
blob: Blob;
responseUrl: string;
}) {
const basename = DownloadPathTemplateEngine(downloadPathTemplate, episode);
const filePath = `${basename}.${getUrlExtension(responseUrl)}`;

const buffer = await blob.arrayBuffer();

try {
await app.vault.createBinary(filePath, buffer);
} catch (error) {
Expand All @@ -148,12 +155,15 @@ async function createEpisodeFile({episode, downloadPathTemplate, blob, responseU
downloadedEpisodes.addEpisode(episode, filePath, blob.size);
}

export async function downloadEpisode(episode: Episode, downloadPathTemplate: string) {
export async function downloadEpisode(
episode: Episode,
downloadPathTemplate: string
) {
try {
const { blob, responseUrl } = await downloadFile(episode.streamUrl);

if (!blob.type.contains('audio')) {
throw new Error('Not an audio file.');
if (!blob.type.contains("audio")) {
throw new Error("Not an audio file.");
}

await createEpisodeFile({
Expand All @@ -163,6 +173,8 @@ export async function downloadEpisode(episode: Episode, downloadPathTemplate: st
responseUrl,
});
} catch (error) {
throw new Error(`Failed to download ${episode.title}: ${error.message}`);
throw new Error(
`Failed to download ${episode.title}: ${error.message}`
);
}
}
19 changes: 9 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Episode } from "./types/Episode";
import CurrentEpisodeController from "./store_controllers/CurrentEpisodeController";
import { TimestampTemplateEngine } from "./TemplateEngine";
import createPodcastNote from "./createPodcastNote";
import downloadEpisodeWithProgressNotice from "./downloadEpisode";
import downloadEpisodeWithNotice from "./downloadEpisode";
import DownloadedEpisode from "./types/DownloadedEpisode";
import DownloadedEpisodesController from "./store_controllers/DownloadedEpisodesController";
import { LocalFilesController } from "./store_controllers/LocalFilesController";
Expand Down Expand Up @@ -177,10 +177,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
}

const episode = this.api.podcast;
downloadEpisodeWithProgressNotice(
episode,
this.settings.download.path
);
downloadEpisodeWithNotice(episode, this.settings.download.path);
},
});

Expand Down Expand Up @@ -245,16 +242,16 @@ export default class PodNotes extends Plugin implements IPodNotes {
});

this.addCommand({
id: 'podnotes-toggle-playback',
name: 'Toggle playback',
id: "podnotes-toggle-playback",
name: "Toggle playback",
checkCallback: (checking) => {
if (checking) {
return !!this.api.podcast;
}

this.api.togglePlayback();
}
})
},
});

this.addSettingTab(new PodNotesSettingsTab(this.app, this));

Expand All @@ -266,7 +263,9 @@ export default class PodNotes extends Plugin implements IPodNotes {

this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this));

this.registerObsidianProtocolHandler("podnotes", (action) => podNotesURIHandler(action, this.api));
this.registerObsidianProtocolHandler("podnotes", (action) =>
podNotesURIHandler(action, this.api)
);

this.registerEvent(getContextMenuHandler());
}
Expand Down

0 comments on commit 3acc595

Please sign in to comment.