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
|
| private reconcileTimer?: ReturnType<typeof setInterval>; | ||
| private discoveryTimer?: ReturnType<typeof setInterval>; | ||
| private tickCount = 0; | ||
|
|
||
| constructor(private readonly config: ConfigService) {} | ||
|
|
||
| async onModuleInit(): Promise<void> { | ||
| // Seed initial state from each channel's (free) RSS feed so we are not | ||
| // blank until the first push arrives. | ||
| await this.discovery(); | ||
| this.discoveryTimer = setInterval( | ||
| () => void this.discovery(), | ||
| DISCOVERY_INTERVAL_MS, | ||
| ); | ||
| this.reconcileTimer = setInterval(() => void this.tick(), RECONCILE_TICK_MS); | ||
| } |
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>
| void this.subscribeAll(baseUrl); | ||
| this.renewTimer = setInterval( | ||
| () => void this.subscribeAll(baseUrl), | ||
| RENEW_INTERVAL_MS, | ||
| ); |
…nt; add pruning of expired streams. Update PubSubController to limit processed video IDs and improve signature verification. Refactor PubSubService to handle subscription errors gracefully.
…ubSubService to use stackOf function for improved logging of error details.
…te for channels; update PubSubController to enforce PUBSUB_SECRET validation and improve signature verification. Refactor main.ts to ensure proper raw body parsing for YouTube WebSub pushes.
… PUBSUB_SECRET for push notifications, improve error handling in PubSubController, and adjust raw body parsing in main.ts for HMAC verification.
| const active: string[] = []; | ||
| for (const videos of this.tracked.values()) { | ||
| for (const v of videos.values()) { | ||
| if (v.status === 'live' || v.status === 'upcoming') { | ||
| active.push(v.videoId); | ||
| } | ||
| } | ||
| } |
| // Seed initial state from each channel's (free) RSS feed so we are not | ||
| // blank until the first push arrives. | ||
| await this.discovery(); |
| verify(@Query() query: Record<string, string>): string { | ||
| const challenge = query['hub.challenge']; | ||
| if (!challenge) { | ||
| throw new BadRequestException('Missing hub.challenge'); | ||
| } | ||
| this.logger.log( | ||
| `Verified ${query['hub.mode']} for ${query['hub.topic']}`, | ||
| ); | ||
| return challenge; | ||
| } |
| const params = new URLSearchParams({ | ||
| 'hub.mode': 'subscribe', | ||
| 'hub.topic': topic, | ||
| 'hub.callback': callback, | ||
| 'hub.verify': 'async', | ||
| 'hub.lease_seconds': String(LEASE_SECONDS), | ||
| }); | ||
| if (secret) { | ||
| params.set('hub.secret', secret); | ||
| } |
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.