From e35042500bebac4ee9d39e77abb7eeb469aec8ce Mon Sep 17 00:00:00 2001 From: NeKz Date: Mon, 13 May 2024 09:56:40 +0200 Subject: [PATCH] Support mel.board.portal2.sr --- docker/volumes/initdb/_init.sql | 12 +++ src/bot/commands/render.ts | 11 +++ src/bot/commands/vid.ts | 5 +- src/bot/deno.json | 4 +- src/server/app/views/Status.tsx | 8 +- src/server/app/views/Video.tsx | 10 +-- src/server/deno.json | 6 +- src/server/main.ts | 36 +++++++-- src/server/tasks/board.ts | 35 ++++++++- src/server/tasks/board_insert.ts | 124 +++++++++++++++++-------------- src/server/tasks/mel.ts | 71 ++++++++++++++++++ src/shared/models.ts | 11 +++ 12 files changed, 253 insertions(+), 80 deletions(-) create mode 100644 src/server/tasks/mel.ts diff --git a/docker/volumes/initdb/_init.sql b/docker/volumes/initdb/_init.sql index 09722e1..631d1e4 100644 --- a/docker/volumes/initdb/_init.sql +++ b/docker/volumes/initdb/_init.sql @@ -119,6 +119,18 @@ CREATE TABLE videos ( demo_is_host INT, demo_metadata TEXT, demo_requires_repair INT NOT NULL DEFAULT 0, + board_source INT NOT NULL DEFAULT 0, + board_source_domain VARCHAR(32) AS ( + IF( + board_source = 0, + NULL, + IF( + board_source = 1, + 'board.portal2.sr', + 'mel.board.portal2.sr' + ) + ) + ) STORED, board_changelog_id INT, board_profile_number VARCHAR(32), board_rank INT, diff --git a/src/bot/commands/render.ts b/src/bot/commands/render.ts index 8c3a10a..8df5c06 100644 --- a/src/bot/commands/render.ts +++ b/src/bot/commands/render.ts @@ -44,6 +44,17 @@ const validateUrl = (urlString: string) => { return isNaN(id) ? null : `https://board.portal2.sr/getDemo?id=${id}`; } + // Input/Output: https://mel.board.portal2.sr/getDemo?id=234826 + + if ( + url.origin === 'https://mel.board.portal2.sr' && + url.pathname === '/getDemo' && + url.search.startsWith('?id=') + ) { + const id = parseInt(url.search.slice(4), 10); + return isNaN(id) ? null : `https://mel.board.portal2.sr/getDemo?id=${id}`; + } + // Input: https://autorender.portal2.sr/video.html?v=234826 // Output: https://board.portal2.sr/getDemo?id=234826 diff --git a/src/bot/commands/vid.ts b/src/bot/commands/vid.ts index be1c54d..a5b669c 100644 --- a/src/bot/commands/vid.ts +++ b/src/bot/commands/vid.ts @@ -81,6 +81,7 @@ createCommand({ user_id: string; views: number; share_id: string; + source: string; }[]; } @@ -99,14 +100,14 @@ createCommand({ const map = escapeMaskedLink(video.map); const mapLink = escapeMaskedLink( - `https://board.portal2.sr/chamber/${video.map_id}`, + `https://${video.source}/chamber/${video.map_id}`, ); const time = escapeMaskedLink(formatCmTime(video.time)); const videoLink = `${AUTORENDER_PUBLIC_URI}/videos/${video.share_id}`; const playerName = escapeMaskedLink(video.user); - const profileLink = `https://board.portal2.sr/profile/${video.user_id}`; + const profileLink = `https://${video.source}/profile/${video.user_id}`; await bot.helpers.editOriginalInteractionResponse( interaction.token, diff --git a/src/bot/deno.json b/src/bot/deno.json index 5df9da0..19d540c 100644 --- a/src/bot/deno.json +++ b/src/bot/deno.json @@ -1,7 +1,7 @@ { "tasks": { - "dev": "deno run --import-map=../import_map.json --unstable-kv --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/kv,/logs/bot,worker.ts --allow-write=/kv,/logs/bot --allow-net=discord.com,gateway.discord.gg,gateway-us-east1-b.discord.gg,gateway-us-east1-c.discord.gg,gateway-us-east1-d.discord.gg,deno.land,cdn.discord.com,cdn.discordapp.com,board.portal2.sr,autorender.server --watch main.ts", - "prod": "deno run --import-map=../import_map.json --unstable-kv --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/kv,/logs/bot,worker.ts --allow-write=/kv,/logs/bot --allow-net=discord.com,gateway.discord.gg,gateway-us-east1-b.discord.gg,gateway-us-east1-c.discord.gg,gateway-us-east1-d.discord.gg,deno.land,cdn.discord.com,cdn.discordapp.com,board.portal2.sr,autorender.server main.ts" + "dev": "deno run --import-map=../import_map.json --unstable-kv --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/kv,/logs/bot,worker.ts --allow-write=/kv,/logs/bot --allow-net=discord.com,gateway.discord.gg,gateway-us-east1-b.discord.gg,gateway-us-east1-c.discord.gg,gateway-us-east1-d.discord.gg,deno.land,cdn.discord.com,cdn.discordapp.com,board.portal2.sr,mel.board.portal2.sr,autorender.server --watch main.ts", + "prod": "deno run --import-map=../import_map.json --unstable-kv --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/kv,/logs/bot,worker.ts --allow-write=/kv,/logs/bot --allow-net=discord.com,gateway.discord.gg,gateway-us-east1-b.discord.gg,gateway-us-east1-c.discord.gg,gateway-us-east1-d.discord.gg,deno.land,cdn.discord.com,cdn.discordapp.com,board.portal2.sr,mel.board.portal2.sr,autorender.server main.ts" }, "fmt": { "useTabs": false, diff --git a/src/server/app/views/Status.tsx b/src/server/app/views/Status.tsx index 0409ec7..94202c7 100644 --- a/src/server/app/views/Status.tsx +++ b/src/server/app/views/Status.tsx @@ -22,7 +22,10 @@ type QueuedVideo = }; type AutorenderVideo = - & Pick + & Pick< + Video, + 'share_id' | 'title' | 'created_at' | 'pending' | 'render_node' | 'board_source_domain' | 'board_changelog_id' + > & { rendered_by_username: string | null; }; @@ -93,6 +96,7 @@ export const loader: DataLoader = async ({ context }) => { , videos.created_at , videos.pending , videos.render_node + , videos.board_source_domain , videos.board_changelog_id , renderer.username as rendered_by_username from videos @@ -359,7 +363,7 @@ export const Status = () => { {video.board_changelog_id} diff --git a/src/server/app/views/Video.tsx b/src/server/app/views/Video.tsx index 6d403aa..037be2a 100644 --- a/src/server/app/views/Video.tsx +++ b/src/server/app/views/Video.tsx @@ -241,7 +241,7 @@ export const VideoView = () => { @@ -255,7 +255,7 @@ export const VideoView = () => { @@ -269,7 +269,7 @@ export const VideoView = () => { @@ -283,7 +283,7 @@ export const VideoView = () => { ? ( {formatCmTime(data.demo_time_score)} @@ -296,7 +296,7 @@ export const VideoView = () => {
View Changelog diff --git a/src/server/deno.json b/src/server/deno.json index b44601e..2e0b3cc 100644 --- a/src/server/deno.json +++ b/src/server/deno.json @@ -4,11 +4,11 @@ "prod": "deno task stale & deno task board & deno task processing & deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,app/assets,/logs/server,/storage --allow-write=/logs/server,/storage --allow-run=ffprobe --allow-net main.ts", "test": "deno test --import-map=../import_map.json --allow-net --allow-read --allow-env --unsafely-ignore-certificate-errors=autorender.portal2.local", "perm": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example --allow-net=autorender.database:3307 tasks/perm.ts", - "migrate": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/migrate_info.log,/logs/server/migrate_error.log,/storage --allow-write=/logs/server/migrate_info.log,/logs/server/migrate_error.log --allow-write=/logs/server/migrate_info.log,/logs/server/migrate_error.log,/storage --allow-net=autorender.database:3307,autorender.portal2.sr,board.portal2.sr tasks/migrate.ts", + "migrate": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/migrate_info.log,/logs/server/migrate_error.log,/storage --allow-write=/logs/server/migrate_info.log,/logs/server/migrate_error.log --allow-write=/logs/server/migrate_info.log,/logs/server/migrate_error.log,/storage --allow-net=autorender.database:3307,autorender.portal2.sr,board.portal2.sr,mel.board.portal2.sr tasks/migrate.ts", "stale": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/stale_info.log,/logs/server/stale_error.log --allow-write=/logs/server/stale_info.log,/logs/server/stale_error.log --allow-net=autorender.database:3307 tasks/stale.ts", "dev:stale": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/stale_info.log,/logs/server/stale_error.log --allow-write=/logs/server/stale_info.log,/logs/server/stale_error.log --allow-net=autorender.database:3307 --watch tasks/stale.ts", - "board": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-write=/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-net=autorender.database:3307,board.portal2.sr,board.nekz.me tasks/board.ts", - "dev:board": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-write=/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-net=autorender.database:3307,board.portal2.sr,board.nekz.me --watch tasks/board.ts", + "board": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-write=/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-net=autorender.database:3307,board.portal2.sr,mel.board.portal2.sr tasks/board.ts", + "dev:board": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-write=/logs/server/board_info.log,/logs/server/board_error.log,/storage --allow-net=autorender.database:3307,board.portal2.sr,mel.board.portal2.sr --watch tasks/board.ts", "processing": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/processing_info.log,/logs/server/processing_error.log,/storage --allow-write=/logs/server/processing_info.log,/logs/server/processing_error.log,/storage --allow-net=autorender.database:3307 --allow-run=ffprobe,ffmpeg tasks/processing.ts", "dev:processing": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/logs/server/processing_info.log,/logs/server/processing_error.log,/storage --allow-write=/logs/server/processing_info.log,/logs/server/processing_error.log,/storage --allow-net=autorender.database:3307 --allow-run=ffprobe,ffmpeg --watch tasks/processing.ts", "id": "deno run --import-map=../import_map.json --no-prompt --allow-env --allow-read=.env,.env.defaults,.env.example,/storage --allow-net=autorender.database:3307 tasks/id.ts" diff --git a/src/server/main.ts b/src/server/main.ts index 8a307d9..15744de 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -24,6 +24,7 @@ import { AccessToken, AuditSource, AuditType, + BoardSource, DiscordUser, FixedDemoStatus, Game, @@ -60,6 +61,7 @@ import { import { rateLimits } from './rate_limits.ts'; import { fetchDemo, getChangelog } from './tasks/portal2_sr.ts'; import { insertVideo } from './tasks/board_insert.ts'; +import { fetchMelDemo } from './tasks/mel.ts'; const SERVER_HOST = Deno.env.get('SERVER_HOST')!; const SERVER_PORT = parseInt(Deno.env.get('SERVER_PORT')!, 10); @@ -756,6 +758,7 @@ apiV1 Video, | 'video_id' | 'share_id' + | 'board_source' | 'board_changelog_id' | 'file_name' | 'created_at' @@ -764,6 +767,7 @@ apiV1 >( `select BIN_TO_UUID(video_id) as video_id , share_id + , board_source , board_changelog_id , file_name , created_at @@ -783,7 +787,8 @@ apiV1 } if (video.board_changelog_id) { - const { demo, originalFilename } = await fetchDemo(video.board_changelog_id); + const demoFetcher = video.board_source === BoardSource.Mel ? fetchMelDemo : fetchDemo; + const { demo, originalFilename } = await demoFetcher(video.board_changelog_id); const filePath = getDemoFilePath(video.video_id); { @@ -978,7 +983,8 @@ apiV1 const videos = await db.query>( `select board_changelog_id from videos - where board_changelog_id in (${ids.map(() => '?')}) + where board_source = ${BoardSource.Portal2} + and board_changelog_id in (${ids.map(() => '?')}) and video_url is not null`, ids, ); @@ -989,12 +995,19 @@ apiV1 }) // Get the video of a leaderboard run. .get('/video/:boardChangelogId(\\d+)/video', async (ctx) => { + const source = Number(ctx.request.url.searchParams.get('source')) ?? BoardSource.Portal2; + if (isNaN(source) || ![BoardSource.Portal2, BoardSource.Mel].includes(source)) { + return Err(ctx, Status.BadRequest, 'Bad value for source parameter.'); + } + const [video] = await db.query>( `select video_url from videos - where board_changelog_id = ? + where board_source = ? + and board_changelog_id = ? and video_url is not null`, [ + source, ctx.params.boardChangelogId, ], ); @@ -1023,20 +1036,22 @@ apiV1 user_id: string; views: number; share_id: string; + source: string; }[]; } type VideoSelect = & Pick< Video, - | 'comment' | 'created_at' + | 'board_source_domain' | 'board_changelog_id' | 'board_rank' | 'demo_time_score' | 'demo_player_name' | 'board_profile_number' | 'views' + | 'comment' | 'share_id' > & Pick; @@ -1056,6 +1071,7 @@ apiV1 user_id: video.board_profile_number, views: video.views, share_id: video.share_id, + source: video.board_source_domain, }; }; @@ -1064,6 +1080,7 @@ apiV1 if (!query.length) { const videos = await db.query( `select videos.created_at + , videos.board_source_domain , videos.board_changelog_id , videos.board_rank , videos.demo_time_score @@ -1226,6 +1243,7 @@ apiV1 const videos = await db.query( `select videos.created_at + , videos.board_source_domain , videos.board_changelog_id , videos.board_rank , videos.demo_time_score @@ -2209,9 +2227,10 @@ router.get('/storage/demos/:share_id/:fixed(fixed)?', async (ctx) => { } try { - const [video] = await db.query>( + const [video] = await db.query>( `select BIN_TO_UUID(video_id) as video_id , file_name + , board_source_domain , board_changelog_id from videos where share_id = ?`, @@ -2224,7 +2243,7 @@ router.get('/storage/demos/:share_id/:fixed(fixed)?', async (ctx) => { } if (video.board_changelog_id) { - return ctx.response.redirect(`https://board.portal2.sr/getDemo?id=${video.board_changelog_id}`); + return ctx.response.redirect(`https://${video.board_source_domain}/getDemo?id=${video.board_changelog_id}`); } const requestedFixedDemo = ctx.params.fixed !== undefined; @@ -2372,7 +2391,8 @@ router.get('/video.html', useSession, async (ctx) => { const [video] = await db.query>( `select share_id from videos - where board_changelog_id = ?`, + where board_source = ${BoardSource.Portal2} + and board_changelog_id = ?`, [ changelogId, ], @@ -2388,7 +2408,7 @@ router.get('/video.html', useSession, async (ctx) => { return Err(ctx, Status.NotFound, 'Unable to fetch changelog entry.'); } - const shareId = await insertVideo(entry); + const shareId = await insertVideo(BoardSource.Portal2, entry); if (!shareId) { return Err(ctx, Status.NotFound, 'Unable to create video.'); } diff --git a/src/server/tasks/board.ts b/src/server/tasks/board.ts index ae1a0d9..4a8ae93 100644 --- a/src/server/tasks/board.ts +++ b/src/server/tasks/board.ts @@ -3,18 +3,20 @@ * * SPDX-License-Identifier: MIT * - * This checks if there are any videos to render from board.portal2.sr. + * This checks if there are any videos to render from board.portal2.sr and mel.board.portal2.sr. */ import 'dotenv/load.ts'; import { db } from '../db.ts'; -import { PendingStatus } from '~/shared/models.ts'; +import { BoardSource, PendingStatus } from '~/shared/models.ts'; import { installLogger, logger } from '../logger.ts'; import { getChangelog } from './portal2_sr.ts'; import { insertVideo } from './board_insert.ts'; +import { getMelChangelog } from './mel.ts'; const BOARD_INTEGRATION_UPDATE_INTERVAL = 60 * 1_000; const BOARD_INTEGRATION_START_DATE = '2023-08-25'; +const MEL_BOARD_INTEGRATION_START_DATE = '2024-05-12'; const FAILED_RENDER_MIN_RETRY_MINUTES = 15; const FAILED_RENDER_MAX_RETRY_MINUTES = 60; @@ -43,7 +45,28 @@ const checkChangelogUpdates = async () => { continue; } - await insertVideo(entry); + await insertVideo(BoardSource.Portal2, entry); + } +}; + +const checkMelChangelogUpdates = async () => { + const changelog = await getMelChangelog({ + endRank: 1, + maxDaysAgo: 1, + banned: 0, + pending: 0, + }); + + if (!changelog) { + return; + } + + for (const entry of changelog) { + if (entry.time_gained.slice(0, 10) < MEL_BOARD_INTEGRATION_START_DATE) { + continue; + } + + await insertVideo(BoardSource.Mel, entry); } }; @@ -89,6 +112,12 @@ const update = async () => { logger.error(err); } + try { + await checkMelChangelogUpdates(); + } catch (err) { + logger.error(err); + } + try { await resetFailedAutorenders(); } catch (err) { diff --git a/src/server/tasks/board_insert.ts b/src/server/tasks/board_insert.ts index 198dccb..4000f09 100644 --- a/src/server/tasks/board_insert.ts +++ b/src/server/tasks/board_insert.ts @@ -8,6 +8,7 @@ import { db } from '../db.ts'; import { AuditSource, AuditType, + BoardSource, FixedDemoStatus, Game, MapModel, @@ -20,16 +21,21 @@ import { generateShareId, getDemoFilePath, getFixedDemoFilePath } from '../utils import { getDemoInfo } from '../demo.ts'; import { logger } from '../logger.ts'; import { ChangelogEntry, fetchDemo, formatCmTime } from './portal2_sr.ts'; +import { fetchMelDemo } from './mel.ts'; const AUTORENDER_RUN_SKIP_COOP_VIDEOS_CHECK = Deno.env.get('AUTORENDER_RUN_SKIP_COOP_VIDEOS_CHECK')?.toLowerCase() === 'true'; -export const insertVideo = async (entry: ChangelogEntry) => { +export const insertVideo = async (boardSource: BoardSource, entry: ChangelogEntry) => { const [existingVideo] = await db.query( `select 1 - from videos - where board_changelog_id = ?`, - [entry.id], + from videos + where board_source = ? + and board_changelog_id = ?`, + [ + boardSource, + entry.id, + ], ); if (existingVideo) { @@ -60,7 +66,8 @@ export const insertVideo = async (entry: ChangelogEntry) => { }; try { - const { demo, originalFilename } = await fetchDemo(entry.id); + const demoFetcher = boardSource === BoardSource.Mel ? fetchMelDemo : fetchDemo; + const { demo, originalFilename } = await demoFetcher(entry.id); if (!demo.ok) { logger.error(`Unable to download demo`); @@ -97,19 +104,24 @@ export const insertVideo = async (entry: ChangelogEntry) => { const [game] = await db.query( `select game_id - from games - where game_mod = ?`, + from games + where game_mod = ?`, [ demoInfo.gameDir, ], ); + if (!game) { + logger.error(`Invalid game dir: ${demoInfo.gameDir}`); + return null; + } + let [map] = await db.query( `select map_id - , auto_fullbright - from maps - where game_id = ? - and name = ?`, + , auto_fullbright + from maps + where game_id = ? + and name = ?`, [ game!.game_id, demoInfo.fullMapName, @@ -147,10 +159,10 @@ export const insertVideo = async (entry: ChangelogEntry) => { const [newMap] = await db.query( `select map_id - , auto_fullbright - from maps - where game_id = ? - and name = ?`, + , auto_fullbright + from maps + where game_id = ? + and name = ?`, [ game!.game_id, demoInfo.fullMapName, @@ -209,6 +221,7 @@ export const insertVideo = async (entry: ChangelogEntry) => { demoInfo.partnerSteamId, demoInfo.isHost, demoMetadata, + boardSource, boardChangelogId, boardProfileNumber, boardRank, @@ -217,36 +230,37 @@ export const insertVideo = async (entry: ChangelogEntry) => { await db.execute( `insert into videos ( - video_id - , game_id - , map_id - , share_id - , title - , comment - , render_quality - , render_options - , file_name - , file_url - , full_map_name - , demo_size - , demo_map_crc - , demo_game_dir - , demo_playback_time - , demo_required_fix - , demo_tickrate - , demo_portal_score - , demo_time_score - , demo_player_name - , demo_steam_id - , demo_partner_player_name - , demo_partner_steam_id - , demo_is_host - , demo_metadata - , board_changelog_id - , board_profile_number - , board_rank - , pending - ) values (UUID_TO_BIN(?), ${new Array(fields.length - 1).fill('?').join(',')})`, + video_id + , game_id + , map_id + , share_id + , title + , comment + , render_quality + , render_options + , file_name + , file_url + , full_map_name + , demo_size + , demo_map_crc + , demo_game_dir + , demo_playback_time + , demo_required_fix + , demo_tickrate + , demo_portal_score + , demo_time_score + , demo_player_name + , demo_steam_id + , demo_partner_player_name + , demo_partner_steam_id + , demo_is_host + , demo_metadata + , board_source + , board_changelog_id + , board_profile_number + , board_rank + , pending + ) values (UUID_TO_BIN(?), ${new Array(fields.length - 1).fill('?').join(',')})`, fields, ); @@ -255,16 +269,16 @@ export const insertVideo = async (entry: ChangelogEntry) => { try { await db.execute( `insert into audit_logs ( - title - , audit_type - , source - , source_user_id - ) values ( - ? - , ? - , ? - , ? - )`, + title + , audit_type + , source + , source_user_id + ) values ( + ? + , ? + , ? + , ? + )`, [ `Created video ${videoId} automatically`, AuditType.Info, diff --git a/src/server/tasks/mel.ts b/src/server/tasks/mel.ts new file mode 100644 index 0000000..c4aaf3e --- /dev/null +++ b/src/server/tasks/mel.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023-2024, NeKz + * + * SPDX-License-Identifier: MIT + */ + +import { ChangelogEntry, ChangelogOptions } from './portal2_sr.ts'; + +export const MEL_BOARD_BASE_API = 'https://mel.board.portal2.sr'; + +const DEFAULT_ABORT_TIMEOUT_MS = 10_000; + +export const getMelChangelog = async (options?: ChangelogOptions) => { + const params = new URLSearchParams(); + + Object.entries(options ?? {}).forEach(([key, value]) => { + if (value !== undefined) { + params.set(key, value.toString()); + } + }); + + const query = params.toString(); + + const url = `${MEL_BOARD_BASE_API}/changelog/json?${query}`; + + const res = await fetch(url, { + headers: { + 'User-Agent': Deno.env.get('USER_AGENT')!, + }, + signal: AbortSignal.timeout(DEFAULT_ABORT_TIMEOUT_MS), + }); + + if (!res.ok) { + return null; + } + + return await res.json() as ChangelogEntry[]; +}; + +export const fetchMelDemo = async (id: string | number) => { + const res = await fetch(`${MEL_BOARD_BASE_API}/getDemo?id=${id}`, { + method: 'GET', + headers: { + 'User-Agent': Deno.env.get('USER_AGENT')!, + }, + redirect: 'manual', + signal: AbortSignal.timeout(DEFAULT_ABORT_TIMEOUT_MS), + }); + + const location = res.headers.get('Location'); + if (!location) { + throw new Error('Unable to redirect without location.'); + } + + const redirect = new URL(res.url); + redirect.pathname = location; + redirect.search = ''; + + const demo = await fetch(redirect.toString(), { + method: 'GET', + headers: { + 'User-Agent': Deno.env.get('USER_AGENT')!, + }, + signal: AbortSignal.timeout(DEFAULT_ABORT_TIMEOUT_MS), + }); + + return { + demo, + originalFilename: location.slice(location.lastIndexOf('/') + 1), + }; +}; diff --git a/src/shared/models.ts b/src/shared/models.ts index 2e6a11a..8387d99 100644 --- a/src/shared/models.ts +++ b/src/shared/models.ts @@ -151,6 +151,15 @@ export enum RenderQuality { UHD_2160p = '2160p', } +/** + * Source of leaderboard. + */ +export enum BoardSource { + None = 0, + Portal2 = 1, + Mel = 2, +} + /** * Table "videos". */ @@ -189,6 +198,8 @@ export interface Video { demo_is_host: number; demo_metadata: string; demo_requires_repair: number; + board_source: BoardSource; + board_source_domain: string; board_changelog_id: number; board_profile_number: string; board_rank: number;