Phase 5.5 abgeschlossen

- Durchsuchen Menu
- Top Live Streams
- Top Kategorie
- Anzeige Zuschaueranzahl getTopGames(), technisch bedingt mit einer gewissen unschärfe zu den tatsächliche Zuschauer der Kategorie
This commit is contained in:
Andre
2026-04-08 21:05:34 +02:00
parent 2cd5e2fb0c
commit 1f40d25771
13 changed files with 869 additions and 19 deletions
+10
View File
@@ -16,6 +16,8 @@ export const IPC = {
twitchGetFollowed: 'twitch:get-followed',
twitchGetVideos: 'twitch:get-videos',
twitchGetTopGames: 'twitch:get-top-games',
twitchGetTopStreams: 'twitch:get-top-streams',
historyGetProgress: 'history:get-progress',
playbackStartLive: 'playback:start-live',
@@ -45,6 +47,14 @@ export function registerIpcHandlers(
})
ipcMain.handle(IPC.twitchGetFollowed, () => helix.getFollowedWithLiveStatus())
ipcMain.handle(
IPC.twitchGetTopGames,
(_e, cursor?: string) => helix.getTopGames(40, cursor)
)
ipcMain.handle(
IPC.twitchGetTopStreams,
(_e, gameId?: string) => helix.getTopStreams(gameId ? { gameId, limit: 40 } : { limit: 100 })
)
ipcMain.handle(
IPC.twitchGetVideos,
(_e, broadcasterId: string) => helix.getVideos(broadcasterId)
+72
View File
@@ -1,6 +1,8 @@
import type {
FollowedChannelInfo,
GameInfo,
HelixFollowedChannel,
HelixGame,
HelixPaginatedResponse,
HelixStream,
HelixUser,
@@ -127,6 +129,76 @@ export class HelixClient {
}))
}
async getTopGames(
limit = 40,
cursor?: string
): Promise<{ games: GameInfo[]; cursor?: string }> {
const params: Record<string, string> = { first: String(limit) }
if (cursor) params.after = cursor
const data = await this.get<HelixPaginatedResponse<HelixGame>>('/games/top', params)
const games = data.data.map((g) => ({
id: g.id,
name: g.name,
boxArtUrl: g.box_art_url.replace('{width}', '285').replace('{height}', '380')
}))
// Zuschauerzahlen: pro Kategorie die Top-100-Streams abrufen (parallel)
const headers = await this.authHeaders()
const viewerCounts = await Promise.all(
games.map(async (g) => {
try {
const url = new URL(`${BASE}/streams`)
url.searchParams.set('game_id', g.id)
url.searchParams.set('first', '100')
const res = await fetch(url.toString(), { headers })
if (!res.ok) return undefined
const d = (await res.json()) as { data: HelixStream[] }
return d.data.reduce((sum, s) => sum + s.viewer_count, 0)
} catch {
return undefined
}
})
)
return {
games: games.map((g, i) => ({ ...g, viewerCount: viewerCounts[i] })),
cursor: data.pagination?.cursor
}
}
async getTopStreams(opts?: { gameId?: string; limit?: number }): Promise<FollowedChannelInfo[]> {
const params: Record<string, string> = { first: String(opts?.limit ?? 20) }
if (opts?.gameId) params.game_id = opts.gameId
const url = new URL(`${BASE}/streams`)
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
const res = await fetch(url.toString(), { headers: await this.authHeaders() })
if (!res.ok) {
const body = await res.text().catch(() => '')
throw new Error(`Helix /streams → ${res.status}: ${body}`)
}
const data = (await res.json()) as { data: HelixStream[] }
const streams = data.data
const userIds = streams.map((s) => s.user_id)
const avatarMap = await this.getProfileImages(userIds)
return streams.map((s) => ({
broadcasterId: s.user_id,
broadcasterLogin: s.user_login,
broadcasterName: s.user_name,
profileImageUrl: avatarMap.get(s.user_id) ?? '',
isLive: true,
streamTitle: s.title,
gameId: s.game_id,
gameName: s.game_name,
viewerCount: s.viewer_count,
thumbnailUrl: s.thumbnail_url.replace('{width}', '440').replace('{height}', '248'),
startedAt: s.started_at
}))
}
async getFollowedWithLiveStatus(): Promise<FollowedChannelInfo[]> {
const userId = await this.getOwnUserId()
const followed = await this.getAllFollowed(userId)
+18
View File
@@ -16,6 +16,7 @@ export interface HelixStream {
user_id: string
user_login: string
user_name: string
game_id: string
game_name: string
title: string
viewer_count: number
@@ -39,6 +40,22 @@ export interface HelixVideo {
type: 'archive' | 'highlight' | 'upload'
}
export interface HelixGame {
id: string
name: string
box_art_url: string
}
/** Simplified game info für den Renderer. */
export interface GameInfo {
id: string
name: string
/** Boxart-URL bereits auf 285×380 aufgelöst */
boxArtUrl: string
/** Summe der Zuschauer aus den Top-100-Streams der Kategorie */
viewerCount?: number
}
/** Simplified VOD info für den Renderer. */
export interface VodInfo {
id: string
@@ -58,6 +75,7 @@ export interface FollowedChannelInfo {
profileImageUrl: string
isLive: boolean
streamTitle?: string
gameId?: string
gameName?: string
viewerCount?: number
/** 440×248 thumbnail URL (only set when live) */