feat: add read-aloud and media playback support

Add EPUB narration, shared media controls, and native device session integration.

Apply the generated migrations and update all first-party clients alongside the server.

BREAKING CHANGE: introduce incompatible database and API contracts for media playback
This commit is contained in:
neon
2026-09-19 02:02:24 +00:00
parent c292d6ccd2
commit e0d3d42150
844 changed files with 151839 additions and 3415 deletions
+20
View File
@@ -5,6 +5,10 @@ export const appConfig = registerAs('app', () => ({
nodeEnv: process.env.NODE_ENV ?? 'development',
host: process.env.HOST?.trim() || '0.0.0.0',
appUrl: process.env.APP_URL ?? 'http://localhost:5173',
// Private-use scheme the iOS app registers with ASWebAuthenticationSession. Compiled into the
// client binary, so it is the same for every provider on every deployment; configurable only so
// a rebranded fork can change it.
nativeRedirectUri: process.env.NATIVE_REDIRECT_URI?.trim() || 'bookorbit://oauth2-callback',
version: process.env.APP_VERSION ?? 'Local build',
githubReleasesRepo: process.env.GITHUB_RELEASES_REPO?.trim() || 'bookorbit/bookorbit',
githubReleasesToken: process.env.GITHUB_RELEASES_TOKEN?.trim() || undefined,
@@ -44,6 +48,13 @@ export const fileWriteConfig = registerAs('fileWrite', () => ({
maxConcurrentWrites: parsePositiveInteger(process.env.FILE_WRITE_MAX_CONCURRENT_WRITES, 2),
}));
export const audiolessEpubConfig = registerAs('audiolessEpub', () => ({
maxConcurrentBuilds: parsePositiveInteger(process.env.AUDIOLESS_EPUB_MAX_CONCURRENT_BUILDS, 2),
maxSourceEntries: parsePositiveInteger(process.env.AUDIOLESS_EPUB_MAX_SOURCE_ENTRIES, 50_000),
maxMetadataBytes: parsePositiveInteger(process.env.AUDIOLESS_EPUB_MAX_METADATA_BYTES, 16 * 1024 * 1024),
maxOutputBytes: parsePositiveInteger(process.env.AUDIOLESS_EPUB_MAX_OUTPUT_BYTES, 2 * 1024 * 1024 * 1024),
}));
export const emailConfig = registerAs('email', () => ({
encryptionKey: process.env.EMAIL_ENCRYPTION_KEY ?? '',
}));
@@ -62,6 +73,15 @@ export const bookRequestConfig = registerAs('bookRequest', () => ({
encryptionKey: process.env.BOOK_REQUEST_ENCRYPTION_KEY ?? '',
}));
export const podcastConfig = registerAs('podcast', () => ({
encryptionKey: process.env.PODCAST_ENCRYPTION_KEY?.trim() || process.env.JWT_SECRET || 'change-me-in-production',
maxFeedBytes: parsePositiveInteger(process.env.PODCAST_MAX_FEED_BYTES, 10 * 1024 * 1024),
maxEpisodeBytes: parsePositiveInteger(process.env.PODCAST_MAX_EPISODE_BYTES, 2 * 1024 * 1024 * 1024),
maxConcurrentDownloads: parsePositiveInteger(process.env.PODCAST_MAX_CONCURRENT_DOWNLOADS, 2),
requestTimeoutMs: parsePositiveInteger(process.env.PODCAST_REQUEST_TIMEOUT_MS, 30_000),
maxDownloadDurationMs: parsePositiveInteger(process.env.PODCAST_MAX_DOWNLOAD_DURATION_MS, 6 * 60 * 60_000),
}));
export const oidcRuntimeConfig = registerAs('oidcRuntime', () => ({
stateTtlMs: parsePositiveInteger(process.env.OIDC_STATE_TTL_SECS, 300) * 1000,
discoveryCacheTtlMs: parsePositiveInteger(process.env.OIDC_DISCOVERY_CACHE_TTL_SECS, 3600) * 1000,