Skip to content

Commit

Permalink
feat: add support for local files
Browse files Browse the repository at this point in the history
support both playing local files and writing notes & creating timestamps on local files.
  • Loading branch information
chhoumann committed Aug 24, 2022
1 parent 9de763e commit b1bc918
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 12 deletions.
11 changes: 11 additions & 0 deletions src/constants.ts
Expand Up @@ -19,6 +19,13 @@ export const QUEUE_SETTINGS: PlaylistSettings = {
shouldRepeat: false,
}

export const LOCAL_FILES_SETTINGS: PlaylistSettings = {
icon: "folder",
name: "Local Files",
shouldEpisodeRemoveAfterPlay: false,
shouldRepeat: false,
}

export const DEFAULT_SETTINGS: IPodNotesSettings = {
savedFeeds: {},
podNotes: {},
Expand Down Expand Up @@ -50,5 +57,9 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = {
path: "",
},
downloadedEpisodes: {},
localFiles: {
...LOCAL_FILES_SETTINGS,
episodes: [],
}
}

50 changes: 48 additions & 2 deletions src/main.ts
@@ -1,6 +1,6 @@
import FeedParser from 'src/parser/feedParser';
import { currentEpisode, downloadedEpisodes, favorites, playedEpisodes, playlists, queue, savedFeeds, viewState } from 'src/store';
import { Notice, Plugin, WorkspaceLeaf } from 'obsidian';
import { currentEpisode, downloadedEpisodes, favorites, localFiles, playedEpisodes, playlists, queue, savedFeeds, viewState } from 'src/store';
import { Notice, Plugin, TAbstractFile, WorkspaceLeaf } from 'obsidian';
import { API } from 'src/API/API';
import { IAPI } from 'src/API/IAPI';
import { DEFAULT_SETTINGS, VIEW_TYPE } from 'src/constants';
Expand All @@ -27,6 +27,9 @@ import createPodcastNote from './createPodcastNote';
import downloadEpisodeWithProgressNotice from './downloadEpisode';
import DownloadedEpisode from './types/DownloadedEpisode';
import DownloadedEpisodesController from './store_controllers/DownloadedEpisodesController';
import { TFile } from 'obsidian';
import { createUrlObjectFromFilePath } from './utility/createUrlObjectFromFilePath';
import { LocalFilesController } from './store_controllers/LocalFilesController';

export default class PodNotes extends Plugin implements IPodNotes {
public api: IAPI;
Expand All @@ -39,6 +42,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
private playlistController: StoreController<{ [playlistName: string]: Playlist }>;
private queueController: StoreController<Playlist>;
private favoritesController: StoreController<Playlist>;
private localFilesController: StoreController<Playlist>;
private currentEpisodeController: StoreController<Episode>;
private downloadedEpisodesController: StoreController<{ [podcastName: string]: DownloadedEpisode[] }>;

Expand All @@ -52,6 +56,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
playlists.set(this.settings.playlists);
queue.set(this.settings.queue);
favorites.set(this.settings.favorites);
localFiles.set(this.settings.localFiles);
downloadedEpisodes.set(this.settings.downloadedEpisodes);
if (this.settings.currentEpisode) {
currentEpisode.set(this.settings.currentEpisode);
Expand All @@ -62,6 +67,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
this.playlistController = new PlaylistController(playlists, this).on();
this.queueController = new QueueController(queue, this).on();
this.favoritesController = new FavoritesController(favorites, this).on();
this.localFilesController = new LocalFilesController(localFiles, this).on();
this.downloadedEpisodesController = new DownloadedEpisodesController(downloadedEpisodes, this).on();
this.currentEpisodeController = new CurrentEpisodeController(currentEpisode, this).on();

Expand Down Expand Up @@ -214,6 +220,45 @@ export default class PodNotes extends Plugin implements IPodNotes {
new Notice("Episode found, playing now. Please click timestamp again to play at specific time.");
}
});

this.registerEvent(
this.app.workspace.on('file-menu', (menu, file: TAbstractFile) => {
if (!(file instanceof TFile)) return;
if (!file.extension.match(/mp3|mp4|wma|aac|wav|webm|aac|flac|m4a|/)) return;

menu.addItem(item => item
.setIcon('play')
.setTitle('Play with PodNotes')
.onClick(async () => {
const localEpisode: Episode = {
title: file.basename,
description: '',
podcastName: 'local file',
url: app.fileManager.generateMarkdownLink(file, ''),
streamUrl: await createUrlObjectFromFilePath(file.path),
episodeDate: new Date(file.stat.ctime),
}

if (!downloadedEpisodes.isEpisodeDownloaded(localEpisode)) {
downloadedEpisodes.addEpisode(localEpisode, file.path, file.stat.size);
localFiles.update(localFiles => {
localFiles.episodes.push(localEpisode);
return localFiles;
})
}

// A bug occurs where the episode won't play if it has been played.
// This fixes that.
if (get(playedEpisodes)[file.basename]?.finished) {
playedEpisodes.markAsUnplayed(localEpisode);
}

currentEpisode.set(localEpisode);
viewState.set(ViewState.Player);
})
);
})
)
}

onLayoutReady(): void {
Expand All @@ -232,6 +277,7 @@ export default class PodNotes extends Plugin implements IPodNotes {
this?.playlistController.off();
this?.queueController.off();
this?.favoritesController.off();
this?.localFilesController.off();
this?.downloadedEpisodesController.off();
this?.currentEpisodeController.off();
}
Expand Down
8 changes: 8 additions & 0 deletions src/store/index.ts
Expand Up @@ -198,6 +198,14 @@ export const favorites = writable<Playlist>({
shouldRepeat: false,
});

export const localFiles = writable<Playlist>({
icon: 'folder',
name: 'Local Files',
episodes: [],
shouldEpisodeRemoveAfterPlay: false,
shouldRepeat: false,
});

export const playlists = writable<{ [name: string]: Playlist }>({});

export const podcastView = writable<HTMLDivElement>();
Expand Down
24 changes: 24 additions & 0 deletions src/store_controllers/LocalFilesController.ts
@@ -0,0 +1,24 @@
import { StoreController } from 'src/types/StoreController';
import { Playlist } from 'src/types/Playlist';
import { LOCAL_FILES_SETTINGS } from 'src/constants';
import { IPodNotes } from 'src/types/IPodNotes';
import { Writable } from 'svelte/store';

export class LocalFilesController extends StoreController<Playlist> {
private plugin: IPodNotes;

constructor(store: Writable<Playlist>, plugin: IPodNotes) {
super(store)
this.plugin = plugin;
}

protected onChange(value: Playlist) {
this.plugin.settings.localFiles = {
...value,
// To ensure we always keep the correct playlist name
...LOCAL_FILES_SETTINGS
};

this.plugin.saveSettings();
}
}
3 changes: 2 additions & 1 deletion src/types/IPodNotesSettings.ts
Expand Up @@ -15,6 +15,7 @@ export interface IPodNotesSettings {
playlists: { [playlistName: string]: Playlist }
queue: Playlist,
favorites: Playlist,
localFiles: Playlist,
currentEpisode?: Episode,

timestamp: {
Expand All @@ -29,5 +30,5 @@ export interface IPodNotesSettings {
download: {
path: string,
}
downloadedEpisodes: { [podcastName: string]: DownloadedEpisode[] }
downloadedEpisodes: { [podcastName: string]: DownloadedEpisode[] },
}
32 changes: 25 additions & 7 deletions src/ui/PodcastView/EpisodePlayer.svelte
Expand Up @@ -22,7 +22,7 @@
import spawnEpisodeContextMenu from "./spawnEpisodeContextMenu";
import { Episode } from "src/types/Episode";
import { ViewState } from "src/types/ViewState";
import { TFile } from "obsidian";
import { createUrlObjectFromFilePath } from "src/utility/createUrlObjectFromFilePath";
// #region Circumventing the forced two-way binding of the playback rate.
class CircumentForcedTwoWayBinding {
Expand Down Expand Up @@ -111,9 +111,14 @@
srcPromise = getSrc($currentEpisode);
});
const unsubCurrentEpisode = currentEpisode.subscribe(_ => {
srcPromise = getSrc($currentEpisode);
});
return () => {
unsub();
unsubDownloadedSource();
unsubCurrentEpisode();
};
});
Expand Down Expand Up @@ -152,12 +157,7 @@
const downloadedEpisode = downloadedEpisodes.getEpisode(episode);
if (!downloadedEpisode) return '';
const file = app.vault.getAbstractFileByPath(downloadedEpisode.filePath);
if (!file || !(file instanceof TFile)) return '';
const binary = await app.vault.readBinary(file);
return URL.createObjectURL(new Blob([binary], { type: "audio/mpeg" }));
return createUrlObjectFromFilePath(downloadedEpisode.filePath);
} else {
return episode.streamUrl;
}
Expand All @@ -173,12 +173,18 @@
on:mouseenter={() => (isHoveringArtwork = true)}
on:mouseleave={() => (isHoveringArtwork = false)}
>
{#if $currentEpisode.artworkUrl}
<img
class={"podcast-artwork" +
(isHoveringArtwork || $isPaused ? " opacity-50" : "")}
src={$currentEpisode.artworkUrl}
alt={$currentEpisode.title}
/>
{:else}
<div class={"podcast-artwork-placeholder" + (isHoveringArtwork || $isPaused ? " opacity-50" : "")}>
<Icon icon="image" size={150} />
</div>
{/if}
{#if isLoading}
<div class="podcast-artwork-isloading-overlay">
<Loading />
Expand Down Expand Up @@ -301,6 +307,18 @@
position: absolute;
}
.podcast-artwork-placeholder {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
/* Some themes override this, so opting to force like so. */
.podcast-artwork:hover {
cursor: pointer !important;
Expand Down
3 changes: 2 additions & 1 deletion src/ui/PodcastView/PodcastView.svelte
Expand Up @@ -8,6 +8,7 @@
playlists,
queue,
favorites,
localFiles,
podcastView,
viewState,
} from "src/store";
Expand All @@ -34,7 +35,7 @@
onMount(async () => {
const unsubscribePlaylists = playlists.subscribe((pl) => {
displayedPlaylists = [$queue, $favorites, ...Object.values(pl)];
displayedPlaylists = [$queue, $favorites, $localFiles, ...Object.values(pl)];
});
const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => {
Expand Down
2 changes: 1 addition & 1 deletion src/ui/PodcastView/spawnEpisodeContextMenu.ts
Expand Up @@ -53,7 +53,7 @@ export default function spawnEpisodeContextMenu(

menu.addItem(item => item
.setIcon(isDownloaded ? "cross" : "download")
.setTitle(isDownloaded ? "Remove download" : "Download")
.setTitle(isDownloaded ? "Remove file" : "Download")
.onClick(() => {
if (isDownloaded) {
downloadedEpisodes.removeEpisode(episode, true);
Expand Down
10 changes: 10 additions & 0 deletions src/utility/createUrlObjectFromFilePath.ts
@@ -0,0 +1,10 @@
import { TFile } from "obsidian";

export async function createUrlObjectFromFilePath(filePath: string) {
const file = app.vault.getAbstractFileByPath(filePath);
if (!file || !(file instanceof TFile)) return '';

const binary = await app.vault.readBinary(file);

return URL.createObjectURL(new Blob([binary], { type: "audio/mpeg" }));
}

0 comments on commit b1bc918

Please sign in to comment.