Skip to content

Commit

Permalink
feat: upload episode flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Floffah committed Feb 6, 2025
1 parent 0cef2ea commit 082fc3a
Show file tree
Hide file tree
Showing 37 changed files with 2,744 additions and 135 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ Proof-of-concept podcast distribution platform for the AtProto.

### File upload

AtCast uses UploadThing instead of storing episodes in the pds. **This will be changed in the future** once I find a good way to store very large files in an AtProto PDS without hitting limits or annoying BlueSky's admins.
The AtCast lexicons and backend support up to 20MB of audio data per episode. This is to ensure that the PDS can handle the data without slowing down.

This is also so that AtCast can provide a kind of CDN interface using regular rest/http/hls, etc, where services (i.e. Spotify) can't interact with the AtProto. Once the PDS issue is solved, the UploadThing repository will be used as a mirror to store generated streamable audio segments.
The 20MB is modeled after 60 minutes of 30kbps 48khz audio compressed in OPUS format. If necessary, this may be increased in the future.

AtCast does not expect the `audio` field of [`live.atcast.show.episode`](./lexicons/live/atcast/show/episode.json) to ever be changed. It will not be set when the record is created, as uploading and encoding is done in the background. These constraints are because AtCast will generate HLS streams from the audio data and store them in the UploadThing repository after the initial upload. If the audio data is changed in the PDS, the HLS streams will be out of sync. AtCast will be upgraded in the future to handle changes and re-generate the HLS streams.

Note that HLS generation is currently not implemented, but in progress. This will require a separate cluster of workers to handle the encoding of the audio data.

### Podcast model

Expand Down
34 changes: 18 additions & 16 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@
"@atcast/lib": "workspace:*",
"@atcast/models": "workspace:*",
"@atcast/prettier-config": "workspace:*",
"@atproto/api": "^0.13.32",
"@atproto/crypto": "^0.4.3",
"@atproto/identity": "^0.4.5",
"@atproto/jwk-jose": "^0.1.3",
"@atproto/lexicon": "^0.4.5",
"@atproto/oauth-client": "^0.3.8",
"@atproto/xrpc": "^0.6.7",
"@atproto/api": "^0.13.33",
"@atproto/crypto": "^0.4.4",
"@atproto/identity": "^0.4.6",
"@atproto/jwk-jose": "^0.1.4",
"@atproto/lexicon": "^0.4.6",
"@atproto/oauth-client": "^0.3.9",
"@atproto/syntax": "^0.3.2",
"@atproto/xrpc": "^0.6.8",
"@hookform/resolvers": "^3.10.0",
"@iconify/json": "^2.2.302",
"@iconify/json": "^2.2.303",
"@normy/react-query": "^0.18.0",
"@radix-ui/react-accessible-icon": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-accessible-icon": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@react-spring/web": "^9.7.5",
"@stylexjs/stylex": "^0.10.1",
"@tanstack/react-query": "^5.66.0",
Expand All @@ -35,7 +36,7 @@
"lru-cache": "^11.0.2",
"multiformats": "^13.3.1",
"nanoid": "^5.0.9",
"next": "^15.2.0-canary.33",
"next": "^15.2.0-canary.44",
"normalize.css": "^8.0.1",
"pkce-challenge": "^4.1.0",
"react": "^19.0.0",
Expand All @@ -46,13 +47,14 @@
"superjson": "^2.2.2",
"unique-username-generator": "^1.4.0",
"uploadthing": "^7.4.4",
"yup": "^1.6.1"
"yup": "^1.6.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@stylexswc/nextjs-plugin": "^0.6.3",
"@stylexswc/postcss-plugin": "^0.6.3",
"@stylexswc/rs-compiler": "^0.6.3",
"@stylexswc/webpack-plugin": "^0.6.3",
"@stylexswc/nextjs-plugin": "^0.6.4",
"@stylexswc/postcss-plugin": "^0.6.4",
"@stylexswc/rs-compiler": "^0.6.4",
"@stylexswc/webpack-plugin": "^0.6.4",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/bun": "^1.2.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import stylex from "@stylexjs/stylex";

import { colours } from "@/styles/colours.stylex";
import { fontSizes, fontWeights, lineHeights } from "@/styles/fonts.stylex";
import { rounded } from "@/styles/rounded.stylex";
import { sizes } from "@/styles/sizes.stylex";

export function EpisodeProcessingBanner() {
return (
<div {...stylex.props(styles.container)}>
<p {...stylex.props(styles.title)}>Episode still processing</p>
<p {...stylex.props(styles.description)}>
Unfortunately you can&apos;t play this audio right now as
we&apos;re still processing the episode. Please check back
later!
</p>
</div>
);
}

const DARK = "@media (prefers-color-scheme: dark)";

const styles = stylex.create({
container: {
display: "flex",
flexDirection: "column",
padding: sizes.spacing4,
borderRadius: rounded.lg,
backgroundColor: {
default: colours.violet200,
[DARK]: colours.indigo900,
},
border: 1,
borderStyle: "solid",
borderColor: {
default: colours.violet400,
[DARK]: colours.indigo500,
},
},

title: {
fontSize: fontSizes.lg,
lineHeight: lineHeights.lg,
fontWeight: fontWeights.semibold,
},

description: {
fontSize: fontSizes.base,
lineHeight: lineHeights.base,

color: {
default: colours.gray700,
[DARK]: colours.gray300,
},
},
});
29 changes: 29 additions & 0 deletions apps/web/src/app/(core)/[repo]/[id]/AudioPlayerSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AtUri } from "@atproto/api";

import { Episode, User, db } from "@atcast/models";

import { EpisodeProcessingBanner } from "@/app/(core)/[repo]/[id]/AudioPlayerSection/EpisodeProcessingBanner";

export async function AudioPlayerSection({
uri,
user,
internalEpisode: _,
}: {
uri: AtUri;
user: User;
internalEpisode: Episode;
}) {
const processRequests = await db.query.audioProcessRequests.findMany({
where: (audioProcessRequests, { and, eq }) =>
and(
eq(audioProcessRequests.episodeId, uri.rkey),
eq(audioProcessRequests.userId, user.id),
),
});

if (processRequests.length > 0) {
return <EpisodeProcessingBanner />;
}

return null;
}
43 changes: 43 additions & 0 deletions apps/web/src/app/(core)/[repo]/[id]/AudioPlayerSection/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import stylex from "@stylexjs/stylex";

import { colours } from "@/styles/colours.stylex";
import { rounded } from "@/styles/rounded.stylex";
import { sizes } from "@/styles/sizes.stylex";

export function EpisodeTitleSectionLoading() {
return (
<section {...stylex.props(styles.container)}>
<div {...stylex.props(styles.audioPlayerSkeleton)} />
</section>
);
}

const pulseAnimation = stylex.keyframes({
"0%, 100%": {
opacity: 0.5,
},
"50%": {
opacity: 1,
},
});

const DARK = "@media (prefers-color-scheme: dark)";

const styles = stylex.create({
container: {
display: "flex",
flexDirection: "column",
gap: sizes.spacing4,
},

audioPlayerSkeleton: {
backgroundColor: {
default: colours.gray100,
[DARK]: colours.gray900,
},
animation: `${pulseAnimation} 4s cubic-bezier(0.4, 0, 0.6, 1) infinite`,
width: "100%",
height: sizes.spacing40,
borderRadius: rounded.lg,
},
});
79 changes: 79 additions & 0 deletions apps/web/src/app/(core)/[repo]/[id]/EpisodeTitleSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { AtUri } from "@atproto/api";
import stylex from "@stylexjs/stylex";

import { LiveAtcastShowEpisode } from "@atcast/atproto";

import { createBskyClient } from "@/lib/api/bskyClient";
import { colours } from "@/styles/colours.stylex";
import { fontSizes, fontWeights, lineHeights } from "@/styles/fonts.stylex";
import { sizes } from "@/styles/sizes.stylex";

export async function EpisodeTitleSection({
uri,
episode,
}: {
uri: AtUri;
episode: LiveAtcastShowEpisode.Record;
}) {
await new Promise((resolve) => setTimeout(resolve, 5000));

const atprotoClient = createBskyClient();

const profileRecord = await atprotoClient.app.bsky.actor.getProfile({
actor: uri.host,
});

return (
<section {...stylex.props(styles.container)}>
<div {...stylex.props(styles.titleLine)}>
<h2 {...stylex.props(styles.title)}>{episode.title}</h2>
<p {...stylex.props(styles.authorSubtitle)}>
On {profileRecord.data.displayName}
</p>
</div>

{episode.description && (
<p {...stylex.props(styles.description)}>
{episode.description}
</p>
)}
</section>
);
}

const DARK = "@media (prefers-color-scheme: dark)";

const styles = stylex.create({
container: {
display: "flex",
flexDirection: "column",
gap: sizes.spacing4,
},

titleLine: {
display: "flex",
gap: sizes.spacing2,
alignItems: "center",
},

title: {
fontSize: fontSizes.xl,
lineHeight: fontSizes.xl,
},

authorSubtitle: {
fontSize: fontSizes.sm,
lineHeight: lineHeights.sm,
color: colours.gray500,
fontWeight: fontWeights.semibold,
},

description: {
fontSize: fontSizes.base,
lineHeight: lineHeights.base,
color: {
default: colours.gray700,
[DARK]: colours.gray300,
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import stylex from "@stylexjs/stylex";

import { colours } from "@/styles/colours.stylex";
import { fontSizes } from "@/styles/fonts.stylex";
import { rounded } from "@/styles/rounded.stylex";
import { sizes } from "@/styles/sizes.stylex";

export function EpisodeTitleSectionLoading() {
return (
<section {...stylex.props(styles.container)}>
<div {...stylex.props(styles.titleSkeleton)} />
<div {...stylex.props(styles.descriptionSkeleton)} />
</section>
);
}

const pulseAnimation = stylex.keyframes({
"0%, 100%": {
opacity: 0.5,
},
"50%": {
opacity: 1,
},
});

const DARK = "@media (prefers-color-scheme: dark)";

const styles = stylex.create({
container: {
display: "flex",
flexDirection: "column",
gap: sizes.spacing4,
},

titleSkeleton: {
backgroundColor: {
default: colours.gray100,
[DARK]: colours.gray900,
},
animation: `${pulseAnimation} 4s cubic-bezier(0.4, 0, 0.6, 1) infinite`,
width: sizes.spacing40,
height: fontSizes.xl,
borderRadius: rounded.lg,
},

descriptionSkeleton: {
backgroundColor: {
default: colours.gray100,
[DARK]: colours.gray900,
},
animation: `${pulseAnimation} 4s cubic-bezier(0.4, 0, 0.6, 1) infinite`,
width: "100%",
maxWidth: sizes.spacing96,
height: sizes.spacing40,
borderRadius: rounded.lg,
},
});
Loading

0 comments on commit 082fc3a

Please sign in to comment.