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
27 changes: 16 additions & 11 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,57 @@ The one goal for PodNotes is to make it easier to write notes on podcasts.
Here are the features that will help you do that 👇.

## Features

- Mobile friendly
- Podcast player built into Obsidian
- Add any publicly available podcast through search
- Add any publicly available podcast through search, or custom feeds by URL
- Track played episodes & playback progress
- API that can be used by plugins like [QuickAdd](https://github.com/chhoumann/QuickAdd) or [Templater](https://github.com/silentvoid13/Templater). With it, you can get
- Episode title
- Episode description
- Episode URL
- Episode podcast name
- Episode publish date
- Current playback time, both raw and formatted (with customn formatting). This can link to the episode at a given time, which PodNotes can pick up and play
- API allows controlling the player
- Create podcast notes from templates with metadata about episodes
- Capture timestamps & link directly to the time in the episode
- API that can be used by plugins like [QuickAdd](https://github.com/chhoumann/QuickAdd) or [Templater](https://github.com/silentvoid13/Templater) for custom workflows

## Installation
This plugin is currently not in the Obsidian plugin repository. Therefore, you'll need to install it manually.

This plugin is in the Obsidian community plugin store. You can find it by searching in the store, or by clicking [here](obsidian://show-plugin?id=podnotes).

### Installation with BRAT

BRAT is an Obsidian plugin that helps you test beta plugins and themes. Click [here](obsidian://show-plugin?id=obsidian42-brat) to install it in Obsidian.

Add `chhoumann/PodNotes` to BRAT with the `Add a beta plugin for testing` command.

Now follow the appropriate instructions, which most likely will have you go and enable the plugin once it has finished installing.

### Manual installation

Go to the [releases](https://github.com/chhoumann/podnotes/releases/latest) page.
Download `main.js` and `manifest.json`.
Create a new directory in your Obsidian vaults `.obsidian/plugins/` folder called `podnotes` and place the downloaded files there.

Now refresh the plugins in Obsidian and enable PodNotes.

## Screenshots

### Demo

![Demo](resources/demo.gif)

### Podcast Grid

![Podcast Grid](resources/podcast_grid.png)

### Episode List

![Episode List](resources/episode_list.png)

### Player

![Player](resources/player.png)

### Podcast Note Editing

![Podcast Note Editing](resources/podcast_note.png)

### Podcast search
![Podcast Search](resources/podcast_search.png)

![Podcast Search](resources/podcast_search.png)
49 changes: 49 additions & 0 deletions docs/docs/podcasts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Adding podcasts
You can add podcasts by searching for them in the podcast search box in settings.
This searches the iTunes podcast repository.

It is also possible to add custom podcast feeds by pasting the feed URL in the search box.
PodNotes will try to parse the feed, and if successful, will prompt you to add it to your saved feeds.


## Playlists
PodNotes can create playlists, which are lists of episodes.
For each playlist, you can specify a name and an icon.

The playlists are shown in the episode grid, represented by their icons.

By default, you will have a queue playlist and a favorites playlist.

You can delete playlists by pressing the trash bin icon next to the playlist name.
The icon will change to a checkmark, which should be pressed within a short duration to confirm the deletion.

## Episode list
The episode list is a dynamic view, which will change depending on your selection.

If you have no podcast selected, the episode list will show the latest episodes of all your saved feeds.

If you have a podcast selected, the episode list will show all episodes of that podcast.

And lastly, if you select a playlist, the episode list shows all episodes in that playlist.

## Context menu
You can right-click (desktop) or long-press (mobile) on an episode in the episode list to open the context menu.

The context menu will let you

- Play the episode
- Mark the episode as played
- Add / remove the episode to favorites
- Add / remove the episode to queue
- Add / remove the episode to a playlist

## Player
The player will automatically load and play the current episode.

Clicking on the epsiode image will toggle episode playback.

The player also features a progress bar, which you can use to seek to a specific time in the episode.
You can also press the buttons on the player to skip backwards or forwards in the episode.

Episode playback rate can be controlled by using the playback rate slider.
This is, by default, what you have set in the settings.
25 changes: 25 additions & 0 deletions docs/docs/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
PodNotes can create notes from templates. These templates can contain certain syntax, which will be expanded to metadata about the podcast episode you are listening to.

To use templates, you can use the `Create podcast note` Obsidian command.
This requires you to have defined a template for both the file path and note text.

## File path
This template will be used to create the file path for the note. You can use the following syntax:

- `{{title}}`: The title of the podcast episode.
- `{{podcast}}`: The name of the podcast.

Both syntax items will be formatted such that it is safe to use in a file path.
This means the following characters will be removed: `\ , # % & / { } * < > $ ' " : @ ‣ | ?`.

## Note template
This template will be used to create the note text. You can use the following syntax:

- `{{title}}`: The title of the podcast episode.
- `{{description}}`: The description of the podcast episode.
- You can use `{{description:> }}` to prepend each new line with a `>` (to put the entire description in a blockquote).
- `{{podcast}}`: The name of the podcast.
- `{{url}}`: The URL of the podcast episode.
- `{{date}}`: The publish date of the podcast episode.
- You can use `{{date:format}}` to specify a custom [Moment.js](https://momentjs.com) format. E.g. `{{date:YYYY-MM-DD}}`.
- `{{artwork}}`: The URL of the podcast artwork. If no artwork is found, an empty string will be used.
14 changes: 14 additions & 0 deletions docs/docs/timestamps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Timestamps can be created with the `Capture Timestamp` Obsidian command.

This will make PodNotes capture the current playback time to the active note, in the format given in the plugin settings.

## Settings
For timestamps, you can use the following format strings:

- `{{time}}`: The current playback time. Default format is `HH:mm:ss`.
- `{{linktime}}`: The current playback time, formatted as a link to the current episode. Default format is `HH:mm:ss`.

Both of these allow for custom formatting.
By using `{{time:format}}` or `{{linktime:format}}`, you can specify a custom [Moment.js](https://momentjs.com) format.

For example, you might use `{{time:H\h mm\m ss\s}}` to get the time in the format `0h 20m 37s`.
15 changes: 12 additions & 3 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
site_name: PodNotes
repo_url: https://github.com/chhoumann/podnotes
edit_uri: edit/master/docs/docs
nav:
- Home: index.md
- Podcasts: podcasts.md
- 'Notes':
- Timestamps: timestamps.md
- Templates: templates.md
- Advanced:
- API: api.md
- Usage with QuickAdd: QuickAdd.md
theme:
name: material
palette:
primary: blue
name: material
palette:
primary: blue
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions src/TemplateEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { htmlToMarkdown, Notice } from "obsidian";
import { Episode } from "src/types/Episode";
import Fuse from "fuse.js";
import { plugin } from "src/store";
import { get } from "svelte/store";

interface Tags {
[tag: string]: string | ((...args: unknown[]) => string);
}

function TemplateEngine(template: string, tags: Tags) {
return template.replace(/\{\{(.*?)(:\s*?.+?)?\}\}/g, (match: string, tagId: string, params: string) => {
const tagValue = tags[tagId.toLowerCase()];
if (!tagValue) {
const fuse = new Fuse(Object.keys(tags), {
shouldSort: true,
findAllMatches: false,
threshold: 0.4,
isCaseSensitive: false,
});

const similarTag = fuse.search(tagId);

new Notice(`Tag ${tagId} is invalid.${similarTag.length > 0 ? ` Did you mean ${similarTag[0].item}?` : ""}`);
return match;
}

if (typeof tagValue === 'function') {
if (params) {
// Remove initial colon with splice.
const splitParams = params.slice(1).split(',');
const args = Array.isArray(splitParams) ? splitParams : [params];

return tagValue(...args);
}

return tagValue();
}

return tagValue;
});
}

export function NoteTemplateEngine(template: string, episode: Episode) {
return TemplateEngine(template, {
"title": episode.title,
"description": (prependToLines?: string) => {
if (prependToLines) {
return htmlToMarkdown(episode.description)
.split("\n")
.map(prepend(prependToLines))
.join("\n")
}

return htmlToMarkdown(episode.description)
},
"url": episode.url,
"date": (format?: string) => episode.episodeDate ?
window.moment(episode.episodeDate).format(format ?? "YYYY-MM-DD")
: "",
"podcast": episode.podcastName,
"artwork": episode.artworkUrl ?? "",
});
}

function prepend(prepend: string) {
return (str: string) => `${prepend}${str}`;
}

export function TimestampTemplateEngine(template: string) {
return TemplateEngine(template, {
"time": (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss"),
"linktime": (format?: string) => get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss", true),
});
}

export function FilePathTemplateEngine(template: string, episode: Episode) {
return TemplateEngine(template, {
"title": replaceIllegalFileNameCharactersInString(episode.title),
"podcast": replaceIllegalFileNameCharactersInString(episode.podcastName),
});
}

function replaceIllegalFileNameCharactersInString(string: string) {
return string
.replace(/[\\,#%&{}/*<>$'":@\u2023|?]*/g, '') // Replace illegal file name characters with empty string
.replace(/\n/, ' ') // replace newlines with spaces
.replace(' ', ' '); // replace multiple spaces with single space to make sure we don't have double spaces in the file name
}
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,14 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = {
skipBackwardLength: 15,
skipForwardLength: 15,
currentEpisode: undefined,

timestamp: {
template: "- {{time}} ",
},

note: {
path: "",
template: "",
}
}

69 changes: 68 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { FavoritesController } from './store_controllers/FavoritesController';
import { Episode } from './types/Episode';
import CurrentEpisodeController from './store_controllers/CurrentEpisodeController';
import { ViewState } from './types/ViewState';
import { FilePathTemplateEngine, NoteTemplateEngine, TimestampTemplateEngine } from './TemplateEngine';

export default class PodNotes extends Plugin implements IPodNotes {
public api: IAPI;
Expand All @@ -48,7 +49,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
favorites.set(this.settings.favorites);
if (this.settings.currentEpisode) {
currentEpisode.set(this.settings.currentEpisode);
}
}

this.playedEpisodeController = new EpisodeStatusController(playedEpisodes, this).on();
this.savedFeedsController = new SavedFeedsController(savedFeeds, this).on();
Expand Down Expand Up @@ -113,6 +114,72 @@ export default class PodNotes extends Plugin implements IPodNotes {
//@ts-ignore
this.app.plugins.disablePlugin(id).then(() => this.app.plugins.enablePlugin(id))
}
});

this.addCommand({
id: 'capture-timestamp',
name: 'Capture Timestamp',
editorCheckCallback: (checking, editor, view) => {
if (checking) {
return !!this.api.podcast &&
!!this.settings.timestamp.template;
}

const cursorPos = editor.getCursor();
const capture = TimestampTemplateEngine(
this.settings.timestamp.template,
);

editor.replaceRange(capture, cursorPos);
editor.setCursor(cursorPos.line, cursorPos.ch + capture.length);
}
});

this.addCommand({
id: 'create-podcast-note',
name: 'Create Podcast Note',
checkCallback: (checking) => {
if (checking) {
return !!this.api.podcast &&
!!this.settings.note.path &&
!!this.settings.note.template;
}

(async function createPodcastNote() {
const filePath = FilePathTemplateEngine(
this.settings.note.path,
this.api.podcast
);

const filePathDotMd = filePath.endsWith('.md') ? filePath : `${filePath}.md`;

const content = NoteTemplateEngine(
this.settings.note.template,
this.api.podcast
);

const createOrGetFile = async (path: string, content: string) => {
const file = app.vault.getAbstractFileByPath(path);
if (file) {
new Notice(`Note for "${this.api.podcast.title}" already exists`);
return file;
}

return await this.app.vault.create(path, content);
}

try {
const file = await createOrGetFile(filePathDotMd, content);

this.app.workspace
.getLeaf()
.openFile(file)
} catch (error) {
console.error(error);
new Notice(`Failed to create note: "${filePathDotMd}"`);
}
}).bind(this)();
},
})

this.addSettingTab(new PodNotesSettingsTab(this.app, this));
Expand Down
Loading