mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Real streams initial
This commit is contained in:
parent
bf74c18198
commit
96c62e547b
|
|
@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { href, Link } from "react-router";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { loader as sidebarLoader } from "~/features/sidebar/routes/sidebar";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { weaponCategories } from "~/modules/in-game-lists/weapon-ids";
|
||||
import { mySlugify, navIconUrl, SETTINGS_PAGE, userPage } from "~/utils/urls";
|
||||
|
|
@ -271,10 +272,8 @@ function MenuOverlay({
|
|||
{stream.subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
{stream.badge ? (
|
||||
<span className={styles.streamItemBadge}>
|
||||
{stream.badge}
|
||||
</span>
|
||||
{stream.startsAt < Date.now() ? (
|
||||
<span className={styles.streamItemBadge}>LIVE</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { Link, useFetcher, useLocation, useMatches } from "react-router";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { loader as sidebarLoader } from "~/features/sidebar/routes/sidebar";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import type { Breadcrumb, SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { navIconUrl, SETTINGS_PAGE, userPage } from "~/utils/urls";
|
||||
|
|
@ -185,6 +186,7 @@ export function Layout({
|
|||
const location = useLocation();
|
||||
const sidebarData = useSidebarData();
|
||||
const navOffset = useNavOffset();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const events = sidebarData?.events ?? [];
|
||||
const matchStatus = sidebarData?.matchStatus;
|
||||
|
|
@ -295,10 +297,12 @@ export function Layout({
|
|||
{streams.map((stream) => (
|
||||
<SideNavLink
|
||||
key={stream.id}
|
||||
to=""
|
||||
to={stream.url}
|
||||
imageUrl={stream.imageUrl}
|
||||
subtitle={stream.subtitle}
|
||||
badge={stream.badge}
|
||||
badge={
|
||||
isMounted && stream.startsAt < Date.now() ? "LIVE" : undefined
|
||||
}
|
||||
>
|
||||
{stream.name}
|
||||
</SideNavLink>
|
||||
|
|
|
|||
83
app/features/core/streams/streams.server.ts
Normal file
83
app/features/core/streams/streams.server.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { cachified } from "@epic-web/cachified";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
import { RunningTournaments } from "~/features/tournament-bracket/core/RunningTournaments.server";
|
||||
import type { Tournament } from "~/features/tournament-bracket/core/Tournament";
|
||||
import { Status } from "~/modules/brackets-model";
|
||||
import { cache, ttl } from "~/utils/cache.server";
|
||||
import { tournamentStreamsPage } from "~/utils/urls";
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const MAX_STREAMS = 5;
|
||||
|
||||
export function clearLiveStreamsCache() {
|
||||
cache.delete("live-tournament-streams");
|
||||
}
|
||||
|
||||
export type SidebarStream = {
|
||||
id: number;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
url: string;
|
||||
subtitle: string;
|
||||
startsAt: number;
|
||||
tier: TournamentTierNumber | null;
|
||||
};
|
||||
|
||||
export function getLiveTournamentStreams(): Promise<SidebarStream[]> {
|
||||
return cachified({
|
||||
key: "live-tournament-streams",
|
||||
cache,
|
||||
ttl: ttl(FIVE_MINUTES),
|
||||
async getFreshValue() {
|
||||
const streams: SidebarStream[] = [];
|
||||
|
||||
for (const tournament of RunningTournaments.all) {
|
||||
streams.push({
|
||||
id: tournament.ctx.id,
|
||||
name: tournament.ctx.name,
|
||||
imageUrl: tournament.ctx.logoUrl,
|
||||
url: tournamentStreamsPage(tournament.ctx.id),
|
||||
subtitle: deriveCurrentRound(tournament),
|
||||
startsAt: tournament.ctx.startTime.getTime(),
|
||||
tier: tournament.ctx.tier,
|
||||
});
|
||||
}
|
||||
|
||||
return streams.sort(sortByTierAscending).slice(0, MAX_STREAMS);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// xxx: this could be moved to Tournament class
|
||||
// xxx: not always reporting furthest round
|
||||
function deriveCurrentRound(tournament: Tournament): string {
|
||||
for (const bracket of tournament.brackets) {
|
||||
if (bracket.preview) continue;
|
||||
|
||||
for (const match of bracket.data.match) {
|
||||
const isActive =
|
||||
match.status === Status.Ready || match.status === Status.Running;
|
||||
const hasParticipants = match.opponent1 && match.opponent2;
|
||||
const isNotFinished =
|
||||
!match.opponent1?.result && !match.opponent2?.result;
|
||||
|
||||
if (isActive && hasParticipants && isNotFinished) {
|
||||
const context = tournament.matchContextNamesById(match.id);
|
||||
if (context?.roundNameWithoutMatchIdentifier) {
|
||||
return context.roundNameWithoutMatchIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bracket.name;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function sortByTierAscending(a: SidebarStream, b: SidebarStream): number {
|
||||
if (a.tier === null && b.tier === null) return 0;
|
||||
if (a.tier === null) return 1;
|
||||
if (b.tier === null) return -1;
|
||||
return a.tier - b.tier;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { href, type LoaderFunctionArgs } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import { getLiveTournamentStreams } from "~/features/core/streams/streams.server";
|
||||
import * as FriendRepository from "~/features/friends/FriendRepository.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
|
|
@ -40,7 +41,7 @@ export const loader = async (_args: LoaderFunctionArgs) => {
|
|||
logoUrl: string | null;
|
||||
} | null,
|
||||
friends: [] as ReturnType<typeof resolveFriends>,
|
||||
streams: getMockStreams(),
|
||||
streams: await getLiveTournamentStreams(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +99,7 @@ export const loader = async (_args: LoaderFunctionArgs) => {
|
|||
: null,
|
||||
tournamentMatchStatus,
|
||||
friends,
|
||||
streams: getMockStreams(),
|
||||
streams: await getLiveTournamentStreams(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -220,29 +221,3 @@ function resolveFriends(friendsWithActivity: FriendWithActivity[]) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMockStreams() {
|
||||
return [
|
||||
{
|
||||
id: 3,
|
||||
name: "Paddling Pool 252",
|
||||
imageUrl: "https://i.pravatar.cc/150?u=stream1",
|
||||
subtitle: "Losers Finals",
|
||||
badge: "LIVE",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Splash Go!",
|
||||
imageUrl:
|
||||
"https://liquipedia.net/commons/images/7/73/Splash_Go_allmode.png",
|
||||
subtitle: "Tomorrow, 9:00 AM",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Area Cup",
|
||||
imageUrl:
|
||||
"https://pbs.twimg.com/profile_images/1830601967821017088/4SDZVKdj_400x400.jpg",
|
||||
subtitle: "Saturday, 10 AM",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ class RunningTournamentsRegistry {
|
|||
return this.tournaments.get(tournamentId);
|
||||
}
|
||||
|
||||
get all() {
|
||||
return Array.from(this.tournaments.values());
|
||||
}
|
||||
|
||||
has(tournamentId: number) {
|
||||
return this.tournaments.has(tournamentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { clearLiveStreamsCache } from "~/features/core/streams/streams.server";
|
||||
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
||||
import { getTentativeTier } from "~/features/tournament-organization/core/tentativeTiers.server";
|
||||
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
|
||||
|
|
@ -139,10 +140,31 @@ export function clearAllTournamentDataCache() {
|
|||
|
||||
function syncTournamentToRegistry(tournament: Tournament) {
|
||||
const isRunning = tournament.hasStarted && !tournament.everyBracketOver;
|
||||
const wasInRegistry = RunningTournaments.has(tournament.ctx.id);
|
||||
|
||||
if (isRunning) {
|
||||
RunningTournaments.add(tournament);
|
||||
if (!wasInRegistry) {
|
||||
clearLiveStreamsCache();
|
||||
}
|
||||
} else {
|
||||
if (wasInRegistry) {
|
||||
clearLiveStreamsCache();
|
||||
}
|
||||
RunningTournaments.remove(tournament.ctx.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function primeRunningTournamentsCache() {
|
||||
const tournamentIds = await TournamentRepository.findRunningTournamentIds();
|
||||
|
||||
for (const tournamentId of tournamentIds) {
|
||||
const data = await tournamentData({ user: undefined, tournamentId });
|
||||
if (!data) continue;
|
||||
|
||||
const tournament = new Tournament({ ...data, simulateBrackets: false });
|
||||
syncTournamentToRegistry(tournament);
|
||||
}
|
||||
}
|
||||
|
||||
await primeRunningTournamentsCache();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { sub } from "date-fns";
|
||||
import { type Insertable, type NotNull, sql, type Transaction } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db } from "~/db/sql";
|
||||
|
|
@ -1252,3 +1253,41 @@ export function updateTournamentTier({
|
|||
.where("id", "=", tournamentId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function findRunningTournamentIds() {
|
||||
const now = new Date();
|
||||
const oneDayAgo = sub(now, { days: 1 });
|
||||
|
||||
const rows = await db
|
||||
.selectFrom("Tournament")
|
||||
.innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId")
|
||||
.innerJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEvent.id",
|
||||
"CalendarEventDate.eventId",
|
||||
)
|
||||
.select("Tournament.id")
|
||||
.where("Tournament.isFinalized", "=", 0)
|
||||
.where("CalendarEventDate.startTime", "<", dateToDatabaseTimestamp(now))
|
||||
.where(
|
||||
"CalendarEventDate.startTime",
|
||||
">",
|
||||
dateToDatabaseTimestamp(oneDayAgo),
|
||||
)
|
||||
.where((eb) =>
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom("TournamentStage")
|
||||
.select("TournamentStage.id")
|
||||
.whereRef("TournamentStage.tournamentId", "=", "Tournament.id"),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
sql<number>`json_extract("Tournament"."settings", '$.isTest')`,
|
||||
"is not",
|
||||
1,
|
||||
)
|
||||
.execute();
|
||||
|
||||
return rows.map((row) => row.id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user