Skip to content

Commit 40b2bca

Browse files
committed
✨ Revolutionize the podcast
This makes the podcast available as a via the revolution site. set the SIMPLECAST_API_KEY environment variable in order to use the podcast episodes with the following operation: ```ts yield* usePodcastEpisodes(); ```` If the API key is not set, then you will see ``` simplecast: disabled ``` logged to the console.
1 parent a8573d2 commit 40b2bca

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed

main.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ import { blogPostRoute } from "./routes/blog-post-route.tsx";
1919
import { blogIndexRoute } from "./routes/blog-index-route.tsx";
2020
import { blogTagRoute } from "./routes/blog-tag-route.tsx";
2121
import { initBlog } from "./blog/blog.ts";
22+
import { podcastIndexRoute } from "./routes/podcast-index-route.tsx";
23+
import { initSimpleCast } from "./podcast/podcast.ts";
24+
import { podcastRoute } from "./routes/podcast-route.tsx";
2225

2326
await main(function* () {
2427
let proxies = proxySites();
2528

2629
yield* initBlog();
30+
yield* initSimpleCast(Deno.env.get("SIMPLECAST_API_KEY"));
2731

2832
let revolution = createRevolution({
2933
app: [
@@ -32,6 +36,8 @@ await main(function* () {
3236
route("/blog/:id", blogPostRoute()),
3337
route("/blog/tags/:tag", blogTagRoute()),
3438
route("/blog(.*)", assetsRoute("blog")),
39+
route("/podcast", podcastIndexRoute()),
40+
route("/podcast/:id", podcastRoute()),
3541
route("/backstage", backstageServicesRoute()),
3642
route("/dx-consulting", dxConsultingServicesRoute()),
3743
route("/work/case-studies/resideo", resideoBackstageCaseStudyRoute()),

podcast/podcast.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
call,
3+
createContext,
4+
Operation,
5+
useAbortSignal,
6+
} from "effection";
7+
8+
const PodcastContext = createContext<Episode[]>(
9+
"podcast",
10+
);
11+
12+
export interface SimplecastClient {
13+
getEpisodes(name: string): Operation<Episode[]>;
14+
}
15+
16+
export interface Podcast {
17+
readonly title: string;
18+
readonly id: string;
19+
}
20+
21+
export interface Episode {
22+
readonly id: string;
23+
readonly title: string;
24+
readonly description: string;
25+
readonly image_url: string;
26+
readonly href: string;
27+
readonly duration: number;
28+
readonly linkname: string;
29+
}
30+
export function* initSimpleCast(apiKey?: string) {
31+
if (!apiKey) {
32+
console.log(`simplecast: disabled`);
33+
yield* PodcastContext.set([]);
34+
} else {
35+
let client = new HTTPClient({ apiKey });
36+
let episodes = yield* client.getEpisodes("The Frontside Podcast");
37+
console.log(`simplecast: loaded ${episodes.length} episodes`);
38+
yield* PodcastContext.set(episodes);
39+
}
40+
}
41+
42+
export function* usePodcastEpisodes(): Operation<Episode[]> {
43+
return yield* PodcastContext;
44+
}
45+
46+
interface HTTPCLientOptions {
47+
apiKey: string;
48+
}
49+
50+
class HTTPClient implements SimplecastClient {
51+
constructor(public readonly options: HTTPCLientOptions) {}
52+
53+
*getEpisodes(title: string): Operation<Episode[]> {
54+
let podcasts = yield* this.getPodcasts();
55+
let podcast = podcasts.find((p) => p.title === title);
56+
if (!podcast) {
57+
throw new Error(
58+
`unable to find podcast: ${title} in [${
59+
podcasts.map((p) => p.title).join(", ")
60+
}]`,
61+
);
62+
}
63+
let response = yield* this.request(`/podcasts/${podcast.id}/episodes`, {
64+
limit: "200",
65+
});
66+
let json = yield* call(() => response.json());
67+
return json.collection.map((episode: Episode) => ({
68+
...episode,
69+
linkname: episode.title.toLowerCase().replaceAll(/\s/g, "-"),
70+
}));
71+
}
72+
73+
*getPodcasts(): Operation<Podcast[]> {
74+
let response = yield* this.request("/podcasts");
75+
let json = yield* call(() => response.json());
76+
return json.collection;
77+
}
78+
79+
private *request(
80+
pathname: string,
81+
params: Record<string, string> = {},
82+
): Operation<Response> {
83+
let url = new URL(`https://api.simplecast.com`);
84+
url.pathname = pathname;
85+
url.search = new URLSearchParams(params).toString();
86+
let signal = yield* useAbortSignal();
87+
let response = yield* call(() =>
88+
fetch(url, {
89+
signal,
90+
headers: {
91+
"Authorization": `Bearer ${this.options.apiKey}`,
92+
},
93+
})
94+
);
95+
if (!response.ok) {
96+
throw new Error(`${response.status}: ${response.statusText}`, {
97+
cause: pathname,
98+
});
99+
} else {
100+
return response;
101+
}
102+
}
103+
}
104+

routes/podcast-index-route.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { type JSXHandler } from "revolution/jsx-runtime";
2+
import { usePodcastEpisodes } from "../podcast/podcast.ts";
3+
4+
export function podcastIndexRoute(): JSXHandler {
5+
return function* () {
6+
let episodes = yield* usePodcastEpisodes();
7+
return (
8+
<html>
9+
<body>
10+
<ol>
11+
{episodes.map((episode) => <li><a href={`podcast/${episode.linkname}`}>{episode.title}</a></li>)}
12+
</ol>
13+
</body>
14+
</html>
15+
);
16+
};
17+
}

routes/podcast-route.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type JSXHandler } from "revolution/jsx-runtime";
2+
import { usePodcastEpisodes } from "../podcast/podcast.ts";
3+
import { respondNotFound, useParams } from "revolution";
4+
5+
export function podcastRoute(): JSXHandler {
6+
return function* () {
7+
let { id } = yield* useParams<{ id: string }>();
8+
let episodes = yield* usePodcastEpisodes();
9+
10+
let episode = episodes.find((episode) => episode.linkname === id);
11+
if (!episode) {
12+
return yield* respondNotFound();
13+
}
14+
15+
return (
16+
<html>
17+
<body>
18+
<h1>{episode.title}</h1>
19+
<ul>
20+
<li><strong>description</strong>: {episode.description}</li>
21+
<li><strong>image_url</strong>: {episode.image_url}</li>
22+
<li><strong>duration</strong>: {episode.duration}</li>
23+
</ul>
24+
</body>
25+
</html>
26+
);
27+
};
28+
}

0 commit comments

Comments
 (0)