* feat: render Spotify and Apple Podcasts embeds on episode pages
Episodes without a Youtube or Simplecast channel rendered no player at
all, since Episode.Body only ever selected those two channels. Spotify
and Apple Podcasts were shown as text links only.
Add an embed fallback so those episodes render the provider's own iframe
player. react-player has no Spotify support, so the embed is a plain
iframe and is intentionally not wired into the global audio player -
these providers expose no playback state to the embedding page.
Also hide the duration in the episode stats row when it is 0, which is
the case for embed players that cannot report a duration.
* fix: drop the Apple Podcasts embed
embed.podcasts.apple.com renders an empty grey placeholder instead of a
player, confirmed on the deployed preview. An Apple-only episode is
better served by the existing channel link than by a broken embed, so
only Spotify falls back to an iframe player.
* feat: play Apple Podcasts episodes from the resolved audio file
Apple's own iframe embed cannot be used: embed.podcasts.apple.com renders
an empty grey placeholder and never mounts its player component. Verified
in a real Chrome, both for our episode and for a mainstream podcast (The
Daily), so it is Apple's embed service rather than our data.
Resolve the episode's audio file instead. The Apple Podcasts URL carries
the show id, and the iTunes lookup API returns the enclosure URL and the
duration for every episode of that show. Exposing that as an audio
channel lets the existing Simplecast code path play it, which also brings
back the duration in the stats row and the global bottom player.
Episode ids in the CMS are not always in sync with Apple, so the lookup
falls back to matching on the episode title.
* fix: prefer the Spotify embed over the resolved audio file
An episode that has both Spotify and Apple Podcasts now renders the
Spotify player. The resolved audio file stays as the last resort, for
episodes that can neither be played nor embedded.
The transformer skips the iTunes lookup for those episodes too, so it
only runs when its result will actually be used.
* chore: ignore env files
Keeps new local env files out of commits. Does not untrack the existing
.env, which is still committed.
Compared docker image built from this branch vs master and saw
significant improvemnts
```
=== FULL A/B MEMORY TABLE ===
Time Master RSS Master Heap Fix RSS Fix Heap RSS
diff
------------------------------------------------------------------------
18:08:21 1194MB 57MB 1424MB 46MB
-231MB
18:08:52 2201MB 60MB 2425MB 77MB
-223MB
18:09:22 2690MB 311MB 2514MB 152MB
+176MB
18:09:53 3230MB 598MB 2597MB 238MB
+633MB
18:10:23 3720MB 994MB 2553MB 109MB
+1167MB
18:10:54 4083MB 1194MB 2627MB 220MB
+1457MB
18:11:24 4404MB 1580MB 2571MB 148MB
+1833MB
```
`src/pages/api/og.tsx`
- Removed `export const config = { runtime: 'edge' }` -> this is the
root cause fix. otherwise `Next.js` compiles the route as a Node.js API
route and next/og uses the native renderer instead of the `WASM` leak.
-
https://github.com/vercel/next.js/blob/canary/packages/next/src/server/og/image-response.ts
-
https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#runtime
- https://github.com/thx/resvg-js/issues/216
- Switched import from `@vercel/og` → `next/og`, and handler signature
from `NextRequest` → `NextApiRequest` / `NextApiResponse`
- Font loading changed: was `fetch(new URL(...))` on every request → now
`fs.readFileSync` cached in module-level `let` variables (`loraBuffer`,
`interBuffer`). These resets to `null` on error so the next request
retries.
- Added Cache-Control: `public, max-age=3600, immutable response
header`: OG images are now CDN-cacheable for 1 hour.
- Added three try-catch blocks:
- font load failure → 500
- ImageResponse construction failure → 500 + cache reset
- response piping failure → 500
- Stored `setInterval` return value in `this._clearCacheInterval`
(private field) instead of discarding it, previously it was an
unreferenceable leak
- Wrapped `checkForUpdate` body in `try-catch`: a transient error (e.g.
file-read failure of webhook data) no longer kills the polling chain,
the setTimeout at the end of the function now always schedules the next
tick
`src/services/post-search.service.ts`
- Added MAX_INDEX_CACHE_SIZE = 50 constant and FIFO eviction: when
this.posts reaches 50 entries, the oldest (first-inserted) key is
deleted before adding the new one, this prevents unbounded `Lunr` index
accumulation over the server lifetime.
`src/services/images.service.ts`
- Added MAX_PLACEHOLDER_CACHE_SIZE = 500 constant and FIFO eviction in
`PlaceholderService.add()`: when this.cache reaches 500 entries, the
oldest Map entry is evicted, this prevents unbounded string accumulation
across all revalidations.