Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
18 changes: 7 additions & 11 deletions src/API/API.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ describe("API volume (AP-07)", () => {
describe("API transcript access", () => {
function setTranscriptVault(files: Record<string, string>) {
const createTFile = (path: string): TFile => Object.assign(new TFile(), { path });
const getAbstractFileByPath = vi.fn((path: string) =>
const getFileByPath = vi.fn((path: string) =>
Object.prototype.hasOwnProperty.call(files, path) ? createTFile(path) : null,
);
const read = vi.fn(async (file: TFile) => files[file.path] ?? "");
Expand All @@ -432,10 +432,10 @@ describe("API transcript access", () => {
path: "Transcripts/{{podcast}}/{{title}}.md",
},
},
{ getAbstractFileByPath, read } as unknown as Partial<Vault>,
{ getFileByPath, read } as unknown as Partial<Vault>,
);

return { api, player, getAbstractFileByPath, read };
return { api, player, getFileByPath, read };
}

test("returns null when no episode is loaded", async () => {
Expand All @@ -445,14 +445,12 @@ describe("API transcript access", () => {
});

test("returns null when the current episode has no generated transcript file", async () => {
const { api, player, getAbstractFileByPath, read } = setTranscriptVault({});
const { api, player, getFileByPath, read } = setTranscriptVault({});
player.restore(feedEpisode);

await expect(api.getTranscript()).resolves.toBeNull();

expect(getAbstractFileByPath).toHaveBeenCalledWith(
"Transcripts/Feed Podcast/Feed Episode.md",
);
expect(getFileByPath).toHaveBeenCalledWith("Transcripts/Feed Podcast/Feed Episode.md");
expect(read).not.toHaveBeenCalled();
});

Expand All @@ -475,15 +473,13 @@ describe("API transcript access", () => {

test("reads the generated transcript note for an explicit episode", async () => {
const transcript = "Local episode transcript";
const { api, player, getAbstractFileByPath } = setTranscriptVault({
const { api, player, getFileByPath } = setTranscriptVault({
"Transcripts/local file/Local Episode.md": transcript,
});
player.restore(feedEpisode);

await expect(api.getTranscript(localEpisode)).resolves.toBe(transcript);

expect(getAbstractFileByPath).toHaveBeenCalledWith(
"Transcripts/local file/Local Episode.md",
);
expect(getFileByPath).toHaveBeenCalledWith("Transcripts/local file/Local Episode.md");
});
});
23 changes: 6 additions & 17 deletions src/API/API.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Episode } from "src/types/Episode";
import { formatEpisodeTimestamp } from "src/utility/formatEpisodeTimestamp";
import type { IAPI } from "./IAPI";
import { TFile } from "obsidian";
import { plugin } from "src/store";
import { get } from "svelte/store";
import { formatPodcastSegment, normalizePodcastSegmentTimes } from "src/utility/podcastSegment";
Expand All @@ -10,8 +9,9 @@ import {
normalizePlaybackRate,
PLAYBACK_RATE_STEP,
} from "src/utility/playbackRate";
import { getEpisodeTranscriptPath } from "src/utility/getEpisodeTranscriptPath";
import { transcriptNote } from "src/services/TranscriptionService";
import { normalizeSkipLength } from "src/utility/skipLength";
import { isPlaying } from "src/player/PlayerController";

export class API implements IAPI {
private get player() {
Expand Down Expand Up @@ -42,10 +42,8 @@ export class API implements IAPI {
this.player.seek(value);
}

/** True while loading too: it plays once the media is ready, and Stop can cancel that. */
public get isPlaying(): boolean {
const { status } = this.state;
return status === "playing" || status === "loading";
return isPlaying(this.state.status);
}

public get volume(): number {
Expand Down Expand Up @@ -111,18 +109,9 @@ export class API implements IAPI {
return null;
}

const pluginInstance = get(plugin);
const transcriptPath = getEpisodeTranscriptPath(
episode,
pluginInstance.settings.transcript.path,
);
const transcriptFile = pluginInstance.app.vault.getAbstractFileByPath(transcriptPath);

if (!(transcriptFile instanceof TFile)) {
return null;
}

return await pluginInstance.app.vault.read(transcriptFile);
const { app, settings } = get(plugin);
const file = transcriptNote(app.vault, settings, episode);
return file ? await app.vault.read(file) : null;
}

start(): void {
Expand Down
2 changes: 2 additions & 0 deletions src/createFeedNote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function setupVault(options: {
}),
},
workspace: {
getActiveViewOfType: () => null,
getLeaf: () => ({
openFile: (file: TFile) => opened.push(file.path),
}),
Expand Down Expand Up @@ -124,6 +125,7 @@ describe("createFeedNote", () => {
createFolder: vi.fn(async () => {}),
},
workspace: {
getActiveViewOfType: () => null,
getLeaf: () => ({
openFile: (file: TFile) => opened.push(file.path),
}),
Expand Down
7 changes: 4 additions & 3 deletions src/createFeedNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { plugin } from "./store";
import addExtension from "./utility/addExtension";
import { enforceMaxPathLength } from "./utility/enforceMaxPathLength";
import { ensureFolderExists } from "./utility/ensureFolderExists";
import { openNote } from "./ui/openNote";

/**
* Resolve the on-disk path of a feed's note from the feed-note path template.
Expand Down Expand Up @@ -42,7 +43,7 @@ export function openFeedNote(feed: PodcastFeed): void {
return;
}

void get(plugin).app.workspace.getLeaf().openFile(file);
void openNote(get(plugin).app, file);
}

export default async function createFeedNote(feed: PodcastFeed): Promise<void> {
Expand All @@ -57,7 +58,7 @@ export default async function createFeedNote(feed: PodcastFeed): Promise<void> {
const existing = getFeedNote(feed);
if (existing) {
new Notice(`Note for "${feed.title}" already exists`);
void get(plugin).app.workspace.getLeaf().openFile(existing);
void openNote(get(plugin).app, existing);
return;
}

Expand All @@ -71,7 +72,7 @@ export default async function createFeedNote(feed: PodcastFeed): Promise<void> {

try {
const file = await createFileIfNotExists(filePathDotMd, content, feed);
void get(plugin).app.workspace.getLeaf().openFile(file);
void openNote(get(plugin).app, file);
} catch (error) {
console.error(error);
new Notice(`Failed to create note: "${filePathDotMd}"`);
Expand Down
18 changes: 14 additions & 4 deletions src/createPodcastNote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function bootstrapAppMock(existingFile?: TFile) {
}),
},
workspace: {
getActiveViewOfType: () => null,
getLeaf: vi.fn(() => leaf),
},
};
Expand Down Expand Up @@ -163,7 +164,7 @@ describe("a note another writer creates at the path first", () => {
throw new Error("File already exists.");
}),
},
workspace: { getLeaf: vi.fn(() => leaf) },
workspace: { getActiveViewOfType: () => null, getLeaf: vi.fn(() => leaf) },
},
settings: { note: { path: "PodNotes/{{title}}", template: "# {{title}}" } },
} as never);
Expand Down Expand Up @@ -274,7 +275,10 @@ describe("getPodcastNote title fallbacks (#315)", () => {
return { path };
}),
},
workspace: { getLeaf: vi.fn(() => ({ openFile: vi.fn() })) },
workspace: {
getActiveViewOfType: () => null,
getLeaf: vi.fn(() => ({ openFile: vi.fn() })),
},
},
settings: {
note: { path: "podcasts/{{podcast}} - {{title}}", template: "# {{title}}" },
Expand Down Expand Up @@ -346,7 +350,10 @@ describe("getPodcastNote title fallbacks (#315)", () => {
return { path };
}),
},
workspace: { getLeaf: vi.fn(() => ({ openFile: vi.fn() })) },
workspace: {
getActiveViewOfType: () => null,
getLeaf: vi.fn(() => ({ openFile: vi.fn() })),
},
},
settings: {
note: { path: "podcasts/{{podcast}} - {{title}}", template: "# {{title}}" },
Expand Down Expand Up @@ -464,7 +471,10 @@ describe("episode notes identified by guid (#349)", () => {
frontmatter: notes.find((note) => note.path === file.path)?.frontmatter,
}),
},
workspace: { getLeaf: vi.fn(() => ({ openFile: vi.fn() })) },
workspace: {
getActiveViewOfType: () => null,
getLeaf: vi.fn(() => ({ openFile: vi.fn() })),
},
},
settings: { note: { path: "podcasts/{{title}}", template } },
} as never);
Expand Down
5 changes: 3 additions & 2 deletions src/createPodcastNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { findUniqueTitleMatch } from "./utility/episodeTitleMatch";
import { findByGuid } from "./utility/findByGuid";
import { addFrontmatterFields } from "./utility/noteFrontmatter";
import { vaultSafeLinkTarget } from "./services/privateFeeds";
import { openNote } from "./ui/openNote";

/**
* Resolve the on-disk path of an episode's note from the configured template.
Expand Down Expand Up @@ -79,7 +80,7 @@ export default async function createPodcastNote(episode: Episode): Promise<void>
const { file, created } = await createPodcastNoteFileIfNotExists(episode);
if (!created) new Notice(`Note for "${episode.title}" already exists`);

void get(plugin).app.workspace.getLeaf().openFile(file);
void openNote(get(plugin).app, file);
} catch (error) {
console.error(error);
new Notice(`Failed to create note: "${getPodcastNotePath(episode)}"`);
Expand Down Expand Up @@ -206,7 +207,7 @@ export function openPodcastNote(episode: Episode): void {
return;
}

void get(plugin).app.workspace.getLeaf().openFile(file);
void openNote(get(plugin).app, file);
}

async function createFileIfNotExists(
Expand Down
17 changes: 16 additions & 1 deletion src/player/PlayerController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { IPodNotesSettings } from "src/types/IPodNotesSettings";
import { getEpisodeKey } from "src/utility/episodeKey";
import { privateFeedPlaceholder } from "src/utility/privateFeedUrl";
import { FakeDocument, FakeParent, type FakeMedia } from "../../tests/mocks/fakeMedia";
import { PlayerController } from "./PlayerController";
import { isPlaying, PlayerController, type PlayerStatus } from "./PlayerController";

const episode = (title: string, extra: Partial<Episode> = {}): Episode => ({
title,
Expand Down Expand Up @@ -117,6 +117,21 @@ function seekedByBrowser(media: FakeMedia, time: number, at = now) {

const flush = () => new Promise((resolve) => setTimeout(resolve, 0));

test("counts a loading episode as playing, and no other status", () => {
const playing: Record<PlayerStatus, boolean> = {
empty: false,
restored: false,
loading: true,
playing: true,
paused: false,
ended: false,
error: false,
};

for (const [status, expected] of Object.entries(playing))
expect(isPlaying(status as PlayerStatus), status).toBe(expected);
});

describe("synced progress handoff", () => {
function paused() {
const context = setup();
Expand Down
5 changes: 5 additions & 0 deletions src/player/PlayerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export type PlayerStatus =
| "ended"
| "error";

/** A loading episode plays once ready, so it counts as playing: it shows Pause, and Stop cancels the load. */
export function isPlaying(status: PlayerStatus): status is "playing" | "loading" {
return status === "playing" || status === "loading";
}

export interface PlayerState {
status: PlayerStatus;
episode: Episode | undefined;
Expand Down
14 changes: 14 additions & 0 deletions src/player/mediaSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ describe("what the lock screen shows", () => {
expect(context.session.position).toEqual({ duration: 3600, playbackRate: 1, position: 40 });
});

test("an episode still loading shows as playing, and one that ended as paused", () => {
const context = setup();

context.player.play(A);
expect(context.state().status).toBe("loading");
expect(context.session.playbackState).toBe("playing");

loaded(context.media(), 60);
reach(context.media(), 60);
context.media().emit("ended");
expect(context.state().status).toBe("ended");
expect(context.session.playbackState).toBe("paused");
});

test("the position follows the speed, and a pause", () => {
const context = setup();
playing(context, 40);
Expand Down
18 changes: 13 additions & 5 deletions src/player/mediaSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import type { Episode } from "src/types/Episode";
import type { NextTrackAction, PreviousTrackAction } from "src/types/Headset";
import type { IPodNotesSettings } from "src/types/IPodNotesSettings";
import { getEpisodeKey } from "src/utility/episodeKey";
import type { PlayerController, PlayerState, PlayerStatus } from "./PlayerController";
import {
isPlaying,
type PlayerController,
type PlayerState,
type PlayerStatus,
} from "./PlayerController";

interface MediaSessionHost {
mediaSession: MediaSession | undefined;
Expand All @@ -19,11 +24,12 @@ interface MediaSessionHost {
const CHAPTER_RESTART_S = 2;
const CHAPTER_TOLERANCE_S = 0.25;

const PLAYBACK_STATES: Record<PlayerStatus, MediaSessionPlaybackState> = {
const IDLE_PLAYBACK_STATES: Record<
Exclude<PlayerStatus, "playing" | "loading">,
MediaSessionPlaybackState
> = {
empty: "none",
restored: "none",
loading: "playing",
playing: "playing",
paused: "paused",
ended: "paused",
error: "none",
Expand Down Expand Up @@ -176,7 +182,9 @@ export function bindMediaSession(
}
setHandlers(!!state.episode);
showMetadata(state.episode);
const playbackState = PLAYBACK_STATES[state.status];
const playbackState = isPlaying(state.status)
? "playing"
: IDLE_PLAYBACK_STATES[state.status];
attempt(() => {
mediaSession.playbackState = playbackState;
});
Expand Down
14 changes: 14 additions & 0 deletions src/services/TranscriptionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ describe("TranscriptionService", () => {
]);
});

test("refuses an episode whose note is already at the transcript path", async () => {
const path = "Transcripts/Test Podcast/Test Episode.md";
const service = new TranscriptionService(
createMockPlugin({ existingTranscriptPath: path }),
);

await service.transcribeCurrentEpisode();

expect(noticeMessages).toEqual([
`You've already transcribed this episode - found ${path}.`,
]);
expect(getEpisodeAudioBufferMock).not.toHaveBeenCalled();
});

test("shows notice when no episode is playing", async () => {
const mockPlugin = createMockPlugin({ podcast: null });
const service = new TranscriptionService(mockPlugin);
Expand Down
Loading
Loading