Skip to content

Commit c22d51e

Browse files
committed
Youtube integration and more!
1 parent 9593e5a commit c22d51e

4 files changed

Lines changed: 239 additions & 34 deletions

File tree

src/data/admin-api/event-streams.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,37 @@ export const useRunEventStreamSetter = () => useSupaMutation({
3333
client: FimSupabaseClient,
3434
{ eventIds }: { eventIds: string[] }
3535
) => runEventStreamSetter(client, eventIds)
36+
});
37+
38+
39+
const livestreamDelete = async (
40+
client: FimSupabaseClient,
41+
livestreamId: string
42+
) => {
43+
return fetch(
44+
`${import.meta.env.PUBLIC_ADMIN_API_URL}/api/v1/event-streams/${livestreamId}`,
45+
{
46+
method: "DELETE",
47+
headers: {
48+
"Content-Type": "application/json",
49+
Authorization: `Bearer ${
50+
(await client.auth.getSession()).data.session?.access_token
51+
}`,
52+
},
53+
}
54+
).then(async (resp) => {
55+
if (resp.status === 401 || resp.status === 403)
56+
throw new Error("You do not have permission to perform this action.");
57+
if (!resp.ok)
58+
throw new Error(
59+
`An error occurred while setting the event streams: ${resp.statusText}`
60+
);
61+
});
62+
};
63+
64+
export const useLivestreamDelete = () => useSupaMutation({
65+
mutationFn: (
66+
client: FimSupabaseClient,
67+
{ livestreamId }: { livestreamId: string }
68+
) => livestreamDelete(client, livestreamId)
3669
});

src/data/supabase/av-tools.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ type EventMatchVideoStat = {
1818
latePlayoffVideos: string[] | null,
1919
};
2020

21+
export type EventStream = {
22+
id: string,
23+
event_id: string,
24+
title: string,
25+
platform: string,
26+
channel: string,
27+
url: string,
28+
internal_id: string,
29+
start_time: Date,
30+
}
31+
2132
export const getEventMatchVideoStats = async (client: FimSupabaseClient, onlyCurrent: boolean = true, eventIds?: string[]): Promise<EventMatchVideoStat[]> => {
2233
let query = client
2334
.from("event_match_video_stats")
@@ -65,4 +76,23 @@ const mapDbToEventMatchVideoStat = (db: EventMatchVideoStat): EventMatchVideoSta
6576
numPlayoffVideos: db.numPlayoffVideos,
6677
latePlayoffVideos: db.latePlayoffVideos
6778
} as EventMatchVideoStat;
68-
}
79+
}
80+
81+
const getEventStreamsFromEventIds = async (client: FimSupabaseClient, eventIds: string[]): Promise<EventStream[]> => {
82+
const { data, error } = await client
83+
.from("event_streams")
84+
.select<string, EventStream>("id, event_id, title, platform, channel, url, internal_id, start_time")
85+
.in("event_id", eventIds);
86+
if (error) throw new Error(error.message);
87+
88+
return data;
89+
};
90+
91+
export const getEventStreamsFromEventIdsQueryKey = (...params: Parameters<OmitFirstArg<typeof getEventStreamsFromEventIds>>) => ["getEventStreamsFromEventIds", ...params];
92+
93+
export const useGetEventStreamsFromEventIds = (...params: Parameters<OmitFirstArg<typeof getEventStreamsFromEventIds>>) => useSupaQuery({
94+
queryKey: getEventStreamsFromEventIdsQueryKey(...params),
95+
queryFn: async (client) => {
96+
return await getEventStreamsFromEventIds(client, ...params)
97+
}
98+
});

src/data/supabase/events.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FimSupabaseClient } from "../../supabaseContext";
33
import { useSupaQuery } from "src/hooks/useSupaQuery";
44
import { EventStatus } from "../eventStatus";
55
import { DataSource } from "src/data/admin-api/events.ts";
6+
import { StreamingConfig } from "./truckRoutes";
67

78
export type EventSlim = {
89
id: string
@@ -15,7 +16,8 @@ export type EventSlim = {
1516
status: EventStatus,
1617
truck_routes?: {
1718
id: number,
18-
name: string
19+
name: string,
20+
streaming_config?: StreamingConfig
1921
}
2022
};
2123

@@ -60,7 +62,7 @@ export type EventTeam = {
6062
export const getEventsForSeason = async (client: FimSupabaseClient, seasonId: number): Promise<EventSlim[]> => {
6163
const { data, error } = await client
6264
.from("events")
63-
.select<string, EventSlim>("id,key,code,name,start_time,end_time,status,truck_routes(id,name)")
65+
.select<string, EventSlim>("id,key,code,name,start_time,end_time,status,truck_routes(id,name,streaming_config)")
6466
.order("start_time", {ascending: true})
6567
.order("name", {ascending: true})
6668
.eq('season_id', seasonId);
@@ -191,7 +193,8 @@ export const mapDbToEventSlim = (db: EventSlim): EventSlim => {
191193
status: db.status,
192194
truck_routes: db.truck_routes ? {
193195
id: db.truck_routes.id,
194-
name: db.truck_routes.name
196+
name: db.truck_routes.name,
197+
streaming_config: db.truck_routes.streaming_config ? db.truck_routes.streaming_config : undefined
195198
} : undefined,
196199
} as EventSlim;
197200
}

src/pages/av-tools/event-livestreams.tsx

Lines changed: 169 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Delete, Refresh, YouTube } from "@mui/icons-material";
12
import {
23
FormControl,
34
InputLabel,
@@ -8,36 +9,102 @@ import {
89
ListItemText,
910
Typography,
1011
Button,
12+
Divider,
13+
IconButton,
14+
Tooltip,
15+
CircularProgress,
16+
ListItemIcon,
17+
Box,
1118
} from "@mui/material";
12-
import { isWithinInterval } from "date-fns";
13-
import { useState } from "react";
14-
import { useRunEventStreamSetter } from "src/data/admin-api/event-streams";
19+
import { Fragment, useMemo, useState } from "react";
20+
import { useLivestreamDelete, useRunEventStreamSetter } from "src/data/admin-api/event-streams";
21+
import {
22+
EventStream,
23+
useGetEventStreamsFromEventIds,
24+
} from "src/data/supabase/av-tools";
1525
import { useGetEventsForSeason } from "src/data/supabase/events";
1626
import { useGetSeasons } from "src/data/supabase/seasons";
1727

1828
const EventLivestreams = () => {
29+
// Seasons
1930
const seasons = useGetSeasons();
31+
32+
// Track Selected Season
2033
const [seasonId, setSeasonId] = useState<number | undefined>(
2134
seasons.data?.[seasons.data?.length - 1]?.id
2235
);
36+
37+
// Track loading state for single event updates
38+
const [singleLoading, setSingleLoading] = useState<string>("");
39+
const [singleDelete, setSingleDelete] = useState<string>("");
40+
41+
// Events for Season
2342
const events = useGetEventsForSeason(seasonId ?? -1);
24-
const filteredEvents = (events.data ?? []).filter(
25-
(ev) =>
26-
ev.truck_routes &&
27-
isWithinInterval(new Date(), {
28-
start: ev.start_time,
29-
end: ev.end_time,
30-
})
43+
44+
// Filter to events with routes that are current or upcoming
45+
const filteredEvents = useMemo(
46+
() =>
47+
(events.data ?? []).filter(
48+
(ev) =>
49+
ev.truck_routes &&
50+
(ev.start_time >= new Date() || ev.end_time >= new Date())
51+
),
52+
[events.data]
53+
);
54+
55+
// Get existing streams for these events
56+
const eventStreams = useGetEventStreamsFromEventIds(
57+
filteredEvents.map((ev) => ev.id)
3158
);
3259

60+
// Map of eventId to streams
61+
const streamMap = useMemo(() => {
62+
const map: { [eventId: string]: EventStream[] } = {};
63+
if (eventStreams.data) {
64+
for (const stream of eventStreams.data) {
65+
if (!map[stream.event_id]) map[stream.event_id] = [];
66+
map[stream.event_id].push(stream);
67+
}
68+
}
69+
console.log(map);
70+
return map;
71+
}, [eventStreams.data]);
72+
3373
const setEvents = useRunEventStreamSetter();
74+
const deleteStream = useLivestreamDelete();
3475

3576
const runSetEvents = () => {
36-
setEvents.mutate({
77+
setEvents.mutateAsync({
3778
eventIds: filteredEvents.map((ev) => ev.id),
79+
}).finally(() => {
80+
eventStreams.refetch();
3881
});
3982
};
4083

84+
const runSingleEvent = (eventId: string) => {
85+
setSingleLoading(eventId);
86+
setEvents
87+
.mutateAsync({
88+
eventIds: [eventId],
89+
})
90+
.finally(() => {
91+
eventStreams.refetch();
92+
setSingleLoading("");
93+
});
94+
};
95+
96+
const runDeleteStream = (livestreamId: string) => {
97+
setSingleDelete(livestreamId.toString());
98+
deleteStream
99+
.mutateAsync({
100+
livestreamId,
101+
})
102+
.finally(() => {
103+
eventStreams.refetch();
104+
setSingleDelete("");
105+
});
106+
};
107+
41108
return (
42109
<>
43110
{!seasons.isPending && (
@@ -69,37 +136,109 @@ const EventLivestreams = () => {
69136
{!events.isPending && (
70137
<>
71138
<List>
72-
{(events.data ?? []).length === 0 ? (
139+
{filteredEvents.length === 0 ? (
73140
<ListItem>
74141
<ListItemText primary="No events for this season" />
75142
</ListItem>
76143
) : (
77144
filteredEvents.map((ev: any) => (
78-
<ListItem key={ev.id} divider>
79-
<ListItemText
80-
primary={ev.name}
81-
secondary={
82-
(ev.start_time
83-
? new Date(ev.start_time).toLocaleString()
84-
: "") +
85-
(ev.location ? ` — ${ev.location}` : "") +
86-
(ev.truck_routes ? ` — ${ev.truck_routes.name}` : "")
145+
<Fragment key={ev.id}>
146+
<ListItem
147+
divider
148+
secondaryAction={
149+
<Tooltip title="Update or Create Livestream">
150+
<IconButton
151+
edge="end"
152+
aria-label="delete"
153+
onClick={() => runSingleEvent(ev.id)}
154+
disabled={singleLoading === ev.id}
155+
>
156+
{singleLoading === ev.id ? (
157+
<CircularProgress size={24} />
158+
) : (
159+
<Refresh />
160+
)}
161+
</IconButton>
162+
</Tooltip>
87163
}
88-
/>
89-
</ListItem>
164+
>
165+
<ListItemText
166+
primary={ev.name}
167+
secondary={
168+
(ev.start_time
169+
? new Date(ev.start_time).toLocaleString()
170+
: "") +
171+
(ev.location ? ` — ${ev.location}` : "") +
172+
(ev.truck_routes
173+
? ` — ${ev.truck_routes.name}`
174+
: "") +
175+
(ev.truck_routes?.streaming_config?.Channel_Type
176+
? ` (${ev.truck_routes.streaming_config.Channel_Type} - ${ev.truck_routes.streaming_config.Channel_Id})`
177+
: "")
178+
}
179+
/>
180+
</ListItem>
181+
182+
{streamMap[ev.id] && streamMap[ev.id].length > 0 && (
183+
<List sx={{ pl: 4 }}>
184+
{streamMap[ev.id].map((stream) => (
185+
<ListItem
186+
key={stream.id}
187+
secondaryAction={
188+
<Tooltip title="Delete Livestream">
189+
<IconButton
190+
edge="end"
191+
aria-label="delete"
192+
onClick={() => runDeleteStream(stream.id)}
193+
disabled={singleDelete === stream.id.toString()}
194+
sx={{mr: 10}}
195+
>
196+
{singleDelete === stream.id.toString() ? (
197+
<CircularProgress size={24} />
198+
) : (
199+
<Delete />
200+
)}
201+
</IconButton>
202+
</Tooltip>
203+
}
204+
>
205+
<ListItemIcon>
206+
{stream.platform.toLowerCase() === "twitch" ? (
207+
<img
208+
src="https://pngimg.com/d/twitch_PNG48.png"
209+
alt="Twitch"
210+
style={{ width: 24, height: 24 }}
211+
/>
212+
) : (
213+
<YouTube />
214+
)}
215+
</ListItemIcon>
216+
<ListItemText
217+
primary={stream.title}
218+
secondary={`${stream.platform} - ${stream.url}`}
219+
/>
220+
</ListItem>
221+
))}
222+
</List>
223+
)}
224+
225+
<Divider />
226+
</Fragment>
90227
))
91228
)}
92229
</List>
93230
</>
94231
)}
95232

96-
<Button
97-
variant="contained"
98-
loading={setEvents.isPending}
99-
onClick={runSetEvents}
100-
>
101-
Set Livestreams for Current, Routed Events
102-
</Button>
233+
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
234+
<Button
235+
variant="contained"
236+
loading={setEvents.isPending}
237+
onClick={runSetEvents}
238+
>
239+
Set Livestreams for All Routed Events
240+
</Button>
241+
</Box>
103242
</>
104243
)}
105244
</>

0 commit comments

Comments
 (0)