Skip to content

✨ Revolutionize the podcast #419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: production
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ import { blogPostRoute } from "./routes/blog-post-route.tsx";
import { blogIndexRoute } from "./routes/blog-index-route.tsx";
import { blogTagRoute } from "./routes/blog-tag-route.tsx";
import { initBlog } from "./blog/blog.ts";
import { podcastIndexRoute } from "./routes/podcast-index-route.tsx";
import { initSimpleCast } from "./podcast/podcast.ts";
import { podcastRoute } from "./routes/podcast-route.tsx";

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

yield* initBlog();
yield* initSimpleCast(Deno.env.get("SIMPLECAST_API_KEY"));

let revolution = createRevolution({
app: [
Expand All @@ -32,6 +36,8 @@ await main(function* () {
route("/blog/:id", blogPostRoute()),
route("/blog/tags/:tag", blogTagRoute()),
route("/blog(.*)", assetsRoute("blog")),
route("/podcast", podcastIndexRoute()),
route("/podcast/:id", podcastRoute()),
route("/backstage", backstageServicesRoute()),
route("/dx-consulting", dxConsultingServicesRoute()),
route("/work/case-studies/resideo", resideoBackstageCaseStudyRoute()),
Expand Down
218 changes: 218 additions & 0 deletions podcast/podcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { all, call, createContext, Operation, useAbortSignal } from "effection";

const PodcastContext = createContext<Episode[]>(
"podcast",
);

export interface SimplecastClient {
getEpisodes(name: string): Operation<Episode[]>;
}

export interface Podcast {
readonly title: string;
readonly id: string;
}

export interface Episode {
linkname: string;
season: {
href: string;
number: number;
next_episode_number: number;
};
audio_file_name: string;
is_explicit: boolean;
waveform_pack: string;
audio_file_url: string;
sponsors: {
href: string;
};
number: number;
authors: {
href: string;
collection: Array<
{
href: string;
name: string;
id: string;
}
>;
};
analytics: {
href: string;
};
long_description: string;
podcast: {
id: string;
href: string;
title: string;
status: "published";
image_url: string;
episodes: { count: number };
created_at: string;
account_id: string;
account: {
id: string;
href: string;
owner: {
name: string;
id: string;
email: string;
};
};
};
description: string;
audio_status: "transcoded";
legacy_id: number;
transcription: string | null;
audio_file_size: number;
waveform_json: string;
slug: string;
title: string;
campaign_preview: {
href: string;
};
is_hidden: false;
is_published: true;
warnings: Record<string | number | symbol, string>;
audio_file_path: string;
dashboard_link: string;
audio_content_type: string;
episode_feeds: [
{
id: string;
feed_id: string;
},
];
days_since_release: number;
published_at: string;
href: string;
audio: {
href: string;
};
image_url: string;
id: string;
enclosure_url: string;
ad_free_audio_file_url: string;
duration: number;
keywords: {
href: string;
collection: Array<
{
href: string;
value: string;
id: string;
hide: false;
}
>;
};
token: string;
guid: string;
created_at: string;
image_path: string;
episode_url: string;
audio_file_path_tc: string;
updated_at: string;
audio_file: {
url: string;
size: number;
path_tc: string;
path: string;
name: string;
href: string;
headliner_url: string;
ad_free_url: string;
};
}

export function* initSimpleCast(apiKey?: string) {
if (!apiKey) {
console.log(`simplecast: disabled`);
yield* PodcastContext.set([]);
} else {
let client = new HTTPClient({ apiKey });
let episodes = yield* client.getEpisodes("The Frontside Podcast");
console.dir(episodes[0].linkname);
console.log(`simplecast: loaded ${episodes.length} episodes`);
yield* PodcastContext.set(episodes);
}
}

export function* usePodcastEpisodes(): Operation<Episode[]> {
return yield* PodcastContext;
}

interface HTTPCLientOptions {
apiKey: string;
}

class HTTPClient implements SimplecastClient {
constructor(public readonly options: HTTPCLientOptions) {}

*getEpisodes(title: string): Operation<Episode[]> {
let podcasts = yield* this.getPodcasts();
let podcast = podcasts.find((p) => p.title === title);
if (!podcast) {
throw new Error(
`unable to find podcast: ${title} in [${
podcasts.map((p) => p.title).join(", ")
}]`,
);
}

let response = yield* this.request(
`/podcasts/${podcast.id}/episodes`,
{ limit: 1000, offset: 0 },
);

let json = yield* call(() => response.json());
return (yield* all(
json.collection.map((episodeMetadata: { id: string }) => {
let request = this.request.bind(this);
return call(function* () {
let response = yield* request(`/episodes/${episodeMetadata.id}`);
let episode = yield* call(() => response.json());
return {
...episode,
linkname: episode.title.toLowerCase().replaceAll(/\s/g, "-"),
};
});
}),
)) as Episode[];
}

*getPodcasts(): Operation<Podcast[]> {
let response = yield* this.request("/podcasts");
let json = yield* call(() => response.json());
return json.collection;
}

private *request(
pathname: string,
params: Record<string, string | number> = {},
): Operation<Response> {
let url = new URL(`https://api.simplecast.com`);
url.pathname = pathname;
let searchParams: Record<string, string> = {};
for (let key in params) {
searchParams[key] = String(params[key]);
}
url.search = new URLSearchParams(searchParams).toString();
let signal = yield* useAbortSignal();
let response = yield* call(() =>
fetch(url, {
signal,
headers: {
"Authorization": `Bearer ${this.options.apiKey}`,
},
})
);
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`, {
cause: pathname,
});
} else {
return response;
}
}
}
39 changes: 39 additions & 0 deletions routes/podcast-index-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type JSXHandler } from "revolution/jsx-runtime";
import { usePodcastEpisodes } from "../podcast/podcast.ts";

import { useAppHtml } from "./app.html.tsx";

export function podcastIndexRoute(): JSXHandler {
return function* () {
let AppHtml = yield* useAppHtml({
title:
"The Frontside Podcast | Engineering, Developer Experience, Testing and Tech Leadership",
description:
"The Frontside Podcast dive into engineering, developer experience, and tech leadership. Join industry experts as they share insights on modern software development, testing strategies, and more.",
ogImage: "../assets/img/frontside-logo.png",
twitterXImage: "../assets/img/frontside-logo.png",
author: "Frontside",
});

let episodes = yield* usePodcastEpisodes();

return (
<AppHtml>
<article class="mx-auto container">
<h1 class="ml-12">Podcast</h1>
<ol class="md:gap-6 lg:gap-11 space-y-10 md:space-y-0 md:grid md:grid-cols-2 lg:grid-cols-3 mx-auto p-4 max-w-7xl">
{episodes.map((episode) => (
<li class="flex flex-col border-[#f0f0f0] bg-[#fcfcfc] md:mt-0 p-2 md:p-4 border rounded-md h-full prose">
<a class="no-underline" href={`podcast/${episode.linkname}`}>
<h2>{episode.title}</h2>
<p>{episode.duration}</p>
<p>{episode.description}</p>
</a>
</li>
))}
</ol>
</article>
</AppHtml>
);
};
}
32 changes: 32 additions & 0 deletions routes/podcast-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type JSXHandler } from "revolution/jsx-runtime";
import { usePodcastEpisodes } from "../podcast/podcast.ts";
import { respondNotFound, useParams } from "revolution";

export function podcastRoute(): JSXHandler {
return function* () {
let { id } = yield* useParams<{ id: string }>();
let episodes = yield* usePodcastEpisodes();

let episode = episodes.find((episode) => episode.linkname === id);
if (!episode) {
return yield* respondNotFound();
}

let authors = episode.authors.collection.map(a => a.name).join(", ")

return (
<html>
<body>
<h1>{episode.title}</h1>
<ul>
<li><strong>description</strong>: {episode.description}</li>
<li><strong>image_url</strong>: {episode.image_url}</li>
<li><strong>duration</strong>: {episode.duration}</li>
<li><strong>audio</strong>: {episode.audio_file_url}</li>
<li><strong>authors</strong>: {authors}</li>
</ul>
</body>
</html>
);
};
}
Loading