Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 41 additions & 91 deletions src/TemplateEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,43 @@ function resolveEpisodeNumber(episode: Episode): number | undefined {
return episode.episodeNumber ?? parseEpisodeNumberFromTitle(episode.title);
}

/**
* Build a tag that strips file-name-illegal characters from `rawValue` and, when
* the tag is used with an argument (e.g. `{{title:_}}`), collapses whitespace to
* that replacement. Shared by the file-name {{title}}/{{podcast}} tags.
*/
function legalizedNameTag(rawValue: string): TagValue {
return (whitespaceReplacement?: string) => {
const legal = replaceIllegalFileNameCharactersInString(rawValue);
return whitespaceReplacement
? legal.replace(/\s+/g, whitespaceReplacement)
: legal;
};
}

/**
* Register the file-name-safe episode tags shared by every path/transcript
* template engine: {{title}} and {{podcast}} (illegal-character-stripped, with an
* optional whitespace-replacement arg), {{date}} (episode publish date),
* {{currentdate}}, and {{episodenumber}}. NoteTemplateEngine intentionally does
* NOT use this — there {{title}} is the raw episode title, not a file name.
*/
function addEpisodeFileNameTags(addTag: AddTagFn, episode: Episode): void {
addTag("title", legalizedNameTag(episode.title));
addTag("podcast", legalizedNameTag(episode.podcastName));
addTag("date", (format?: string) =>
episode.episodeDate
? formatDate(episode.episodeDate, format ?? "YYYY-MM-DD")
: "",
);
addTag("currentdate", (format?: string) =>
formatDate(new Date(), format ?? "YYYY-MM-DD"),
);
addTag("episodenumber", (pad?: string) =>
formatEpisodeNumber(resolveEpisodeNumber(episode), pad),
);
}

function formatChapterTitle(title: string): string {
return title.replace(/\s+/g, " ").trim();
}
Expand Down Expand Up @@ -280,35 +317,7 @@ export function TimestampTemplateEngine(
export function FilePathTemplateEngine(template: string, episode: Episode) {
const [replacer, addTag] = useTemplateEngine();

addTag("title", (whitespaceReplacement?: string) => {
const legalTitle = replaceIllegalFileNameCharactersInString(episode.title);
if (whitespaceReplacement) {
return legalTitle.replace(/\s+/g, whitespaceReplacement);
}

return legalTitle;
});
addTag("podcast", (whitespaceReplacement?: string) => {
const legalName = replaceIllegalFileNameCharactersInString(
episode.podcastName,
);
if (whitespaceReplacement) {
return legalName.replace(/\s+/g, whitespaceReplacement);
}

return legalName;
});
addTag("date", (format?: string) =>
episode.episodeDate
? formatDate(episode.episodeDate, format ?? "YYYY-MM-DD")
: "",
);
addTag("currentdate", (format?: string) =>
formatDate(new Date(), format ?? "YYYY-MM-DD"),
);
addTag("episodenumber", (pad?: string) =>
formatEpisodeNumber(resolveEpisodeNumber(episode), pad),
);
addEpisodeFileNameTags(addTag, episode);

return replacer(template);
}
Expand All @@ -322,35 +331,7 @@ export function DownloadPathTemplateEngine(template: string, episode: Episode) {

const [replacer, addTag] = useTemplateEngine();

addTag("title", (whitespaceReplacement?: string) => {
const legalTitle = replaceIllegalFileNameCharactersInString(episode.title);
if (whitespaceReplacement) {
return legalTitle.replace(/\s+/g, whitespaceReplacement);
}

return legalTitle;
});
addTag("podcast", (whitespaceReplacement?: string) => {
const legalName = replaceIllegalFileNameCharactersInString(
episode.podcastName,
);
if (whitespaceReplacement) {
return legalName.replace(/\s+/g, whitespaceReplacement);
}

return legalName;
});
addTag("date", (format?: string) =>
episode.episodeDate
? formatDate(episode.episodeDate, format ?? "YYYY-MM-DD")
: "",
);
addTag("currentdate", (format?: string) =>
formatDate(new Date(), format ?? "YYYY-MM-DD"),
);
addTag("episodenumber", (pad?: string) =>
formatEpisodeNumber(resolveEpisodeNumber(episode), pad),
);
addEpisodeFileNameTags(addTag, episode);

return replacer(templateWithoutExtension);
}
Expand All @@ -362,33 +343,7 @@ export function TranscriptTemplateEngine(
) {
const [replacer, addTag] = useTemplateEngine();

addTag("title", (whitespaceReplacement?: string) => {
const legalTitle = replaceIllegalFileNameCharactersInString(episode.title);
if (whitespaceReplacement) {
return legalTitle.replace(/\s+/g, whitespaceReplacement);
}
return legalTitle;
});
addTag("podcast", (whitespaceReplacement?: string) => {
const legalName = replaceIllegalFileNameCharactersInString(
episode.podcastName,
);
if (whitespaceReplacement) {
return legalName.replace(/\s+/g, whitespaceReplacement);
}
return legalName;
});
addTag("date", (format?: string) =>
episode.episodeDate
? formatDate(episode.episodeDate, format ?? "YYYY-MM-DD")
: "",
);
addTag("currentdate", (format?: string) =>
formatDate(new Date(), format ?? "YYYY-MM-DD"),
);
addTag("episodenumber", (pad?: string) =>
formatEpisodeNumber(resolveEpisodeNumber(episode), pad),
);
addEpisodeFileNameTags(addTag, episode);
addTag("duration", (format?: string) =>
episode.duration !== undefined
? formatDuration(episode.duration, format)
Expand Down Expand Up @@ -453,12 +408,7 @@ export function FeedNoteTemplateEngine(template: string, feed: PodcastFeed) {
export function FeedFilePathTemplateEngine(template: string, feed: PodcastFeed) {
const [replacer, addTag] = useTemplateEngine();

const safeName = replaceIllegalFileNameCharactersInString(feed.title);
const nameTag = (whitespaceReplacement?: string) =>
whitespaceReplacement
? safeName.replace(/\s+/g, whitespaceReplacement)
: safeName;

const nameTag = legalizedNameTag(feed.title);
addTag("title", nameTag);
addTag("podcast", nameTag);
addTag("date", (format?: string) =>
Expand Down
28 changes: 28 additions & 0 deletions src/downloadEpisode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,34 @@ async function createEpisodeFile({
downloadedEpisodes.addEpisode(episode, filePath, data.byteLength);
}

/**
* Remove a downloaded episode: drop it from the offline set and delete its
* backing vault file. This composes the pure store removal with the file I/O so
* callers can't do one without the other (and leak files); the download store
* stays free of vault side effects.
*/
export async function removeDownloadedEpisode(episode: Episode): Promise<void> {
const removedFilePath = downloadedEpisodes.removeEpisode(episode);
if (removedFilePath) {
await deleteEpisodeFile(removedFilePath);
}
}

// Best-effort: a missing/already-removed file is a no-op, and failures are logged
// rather than thrown so removing a stale entry never breaks the calling UI flow.
async function deleteEpisodeFile(filePath: string): Promise<void> {
if (!filePath) return;

try {
const file = app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
await app.vault.delete(file);
}
} catch (error) {
console.error(`Failed to delete downloaded file "${filePath}":`, error);
}
}

function resolveLocalEpisodeFilePath(episode: LocalEpisode): string | null {
const downloadedEpisode = downloadedEpisodes.getEpisode(episode);
const candidatePaths = [
Expand Down
Loading
Loading