feat: add livestream REST endpoints#32
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new NestJS livestream feature area that exposes REST endpoints for querying YouTube livestream state for a configured set of Open Home Foundation project channels, backed by YouTube Data API v3 and cached per channel.
Changes:
- Add
GET /livestreamandGET /livestream/:slugendpoints via a new controller/module/service. - Implement YouTube API integration with per-channel caching and in-flight refresh de-duplication.
- Wire up global config loading (
@nestjs/config) and add developer/supporting files (.env.example, devcontainer port forwarding,.envgitignore).
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/livestream/livestream.service.ts | Implements YouTube API lookups, caching, and status derivation logic for channels. |
| src/livestream/livestream.controller.ts | Adds REST endpoints for listing all channel statuses and querying by slug. |
| src/livestream/livestream.module.ts | Registers the livestream controller/service in a dedicated Nest module. |
| src/livestream/livestream.channels.ts | Defines the config-driven list of tracked channels (slug/name/handle). |
| src/livestream/index.ts | Exposes livestream module/service/channels via barrel exports. |
| src/app.module.ts | Enables global config loading and registers LivestreamModule. |
| package.json | Adds @nestjs/config dependency. |
| pnpm-lock.yaml | Locks @nestjs/config and its transitive dependencies (dotenv, dotenv-expand, etc.). |
| .gitignore | Ensures local .env files are not committed. |
| .env.example | Documents the required YOUTUBE_API_KEY environment variable. |
| .devcontainer.json | Updates forwarded port to 3000 to match the app’s default HTTP port. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
djwmarcx
left a comment
There was a problem hiding this comment.
This solution requires a lot of quota. More than we have available (35x).
We need to come up with a different approach.
What about polling (for each channel):
https://www.youtube.com/feeds/videos.xml?channel_id=UCbX3YkedQunLt7EQAdVxh7w
Plus 1 call to extract the necessary data using the actual API (if really needed)
Switched from
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… PUBSUB_SECRET for push notifications, improve error handling in PubSubController, and adjust raw body parsing in main.ts for HMAC verification.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| const stackOf = (err: unknown): string => | ||
| err instanceof Error ? (err.stack ?? err.message) : String(err); |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| private async subscribeAll(baseUrl: string): Promise<void> { | ||
| const callback = new URL('/pubsub', baseUrl).toString(); | ||
| const secret = this.config.get<string>('PUBSUB_SECRET'); |
| async onModuleInit(): Promise<void> { | ||
| // Seed initial state from each channel's (free) RSS feed so we are not | ||
| // blank until the first push arrives. | ||
| void this.discovery(); | ||
| this.discoveryTimer = setInterval( | ||
| () => void this.discovery(), | ||
| DISCOVERY_INTERVAL_MS, | ||
| ); | ||
| this.reconcileTimer = setInterval(() => void this.tick(), RECONCILE_TICK_MS); | ||
| } |
| const expectedToken = this.config.get<string>('PUBSUB_VERIFY_TOKEN'); | ||
| if (expectedToken && query['hub.verify_token'] !== expectedToken) { | ||
| throw new ForbiddenException('Invalid hub.verify_token'); | ||
| } | ||
|
|
||
| if (mode !== 'subscribe' && mode !== 'unsubscribe') { | ||
| throw new BadRequestException(`Unsupported hub.mode "${mode}"`); | ||
| } | ||
| if (!topic.startsWith('https://www.youtube.com/feeds/videos.xml?channel_id=')) { | ||
| throw new BadRequestException('Unsupported hub.topic'); | ||
| } |
Adds a
livestreamendpoint that reports the YouTube livestream status forOpen Home Foundation project channels (Home Assistant, ESPHome, Open Home
Foundation, Music Assistant). Websites can call this to show upcoming, live, or
recently-ended streams.
Endpoints
GET /livestream— status for all configured channelsGET /livestream/:slug— status for a single channel (e.g.home-assistant);returns
404for unknown slugsEach entry returns a
statusoflive,upcoming,past(within 24h ofending), or
none, plustitle,url, and (for upcoming)startTime.Notes
src/livestream/livestream.channels.ts—adding a project is a one-line change.
a failing channel falls back to stale data and doesn't affect the others.
YOUTUBE_API_KEY(YouTube Data API v3), loaded from.envvia@nestjs/config. See.env.example.3000(the app's default) instead of443.