CJ_Clippy afc9e2d1c8
Some checks failed
ci / build (push) Failing after 2s
ci / Tests & Checks (push) Failing after 1s
add vtuber rss
2025-08-11 20:51:21 -08:00

160 lines
5.0 KiB
TypeScript

import { FastifyInstance, FastifyRequest } from 'fastify'
import { PrismaClient } from '../../generated/prisma'
import { withAccelerate } from "@prisma/extension-accelerate"
import { getOrDownloadAsset } from '../utils/cache'
import { getS3Client } from '../utils/s3'
import { env } from '../config/env'
import { signUrl } from '../utils/cdn'
import { constants } from '../config/constants'
import { readFile } from 'fs/promises'
import { extractBasePath } from '../utils/filesystem'
import { dirname } from 'node:path'
import logger from '../utils/logger'
const prisma = new PrismaClient().$extends(withAccelerate())
const s3 = getS3Client()
// function isMasterPlaylist(content: string): boolean {
// return content
// .split('\n')
// .some(line => !line.startsWith('#') && line.trim().endsWith('.m3u8'))
// }
// function extractTokenParamsFromSignedUrl(signedUrl: string): URLSearchParams {
// const match = signedUrl.match(/\/bcdn_token=[^/]+\/|\/bcdn_token=[^&]+/)
// if (!match) return new URLSearchParams()
// const queryString = match[0].replace(/^\//, '').replace(/\/$/, '')
// return new URLSearchParams(queryString)
// }
// function patchMediaUrlsInPlaylist(
// playlist: string,
// options: { cdnPrefix?: string; queryParams?: URLSearchParams }
// ): string {
// const lines = playlist.split('\n')
// const query = options.queryParams?.toString()
// const cdnPrefix = options.cdnPrefix?.replace(/^https?:\/\/[^/]+/, '').replace(/^\/|\/$/g, '')
// return lines.map(line => {
// if (line.startsWith('#') || line.trim() === '') return line
// if (line.startsWith('http://') || line.startsWith('https://')) {
// try {
// const url = new URL(line)
// if (url.search) return line
// return `${url.origin}${query ? '/' + query : ''}${url.pathname}`
// } catch {
// return line // Not a URL — fallback
// }
// }
// // Relative segment (e.g. `stream_1.ts`)
// let patched = line
// if (cdnPrefix) patched = `${env.CDN_ORIGIN}/${cdnPrefix}/${patched}`
// if (query) patched += `?${query}`
// return patched
// }).join('\n')
// }
function rewriteMp4ReferencesWithSignedUrls(
playlistContent: string,
cdnBasePath: string,
signFn: (path: string) => string
): string {
logger.debug(`rewriteMp4ReferencesWithSignedUrls called with ${playlistContent} ${cdnBasePath} ${signFn}`)
const cleanBase = cdnBasePath.replace(/^\/|\/$/g, '') // remove leading/trailing slash
return playlistContent
.split('\n')
.map((line) => {
const trimmed = line.trim();
// Bare MP4 reference (e.g. "source.mp4")
if (/^[^\/?#]+\.(mp4)$/.test(trimmed)) {
const path = `${cleanBase}/${trimmed}`;
const signedUrl = signFn(path);
return signedUrl;
}
// URI="source.mp4"
return line.replace(/URI="([^"]+\.mp4)"/g, (_, file) => {
const path = `${cleanBase}/${file.replace(/^\/+/, '')}`;
const signedUrl = signFn(path);
return `URI="${signedUrl}"`;
});
})
.join('\n');
}
export default async function registerHlsRoute(app: FastifyInstance) {
app.get('/hls/:vodId/:manifest', async (request, reply) => {
const { vodId, manifest } = request.params as {
vodId: string
manifest: string
}
const vod = await prisma.vod.findFirstOrThrow({
where: { id: vodId },
select: { hlsPlaylist: true },
})
logger.debug(`vod as follows`)
logger.debug(vod)
if (!vod.hlsPlaylist) return reply.status(404).send('')
const s3Key = `${dirname(vod.hlsPlaylist)}/${manifest}`
const filePath = await getOrDownloadAsset(s3, process.env.S3_BUCKET!, s3Key)
const playlistContent = await readFile(filePath, 'utf8')
// If it's the master manifest, just return it as-is
if (manifest === 'master.m3u8') {
return reply
.type('application/x-mpegURL')
.header('Cache-Control', 'public, max-age=60')
.send(playlistContent)
}
// Otherwise, rewrite .mp4 references with signed URLs
const tokenPath = `/${dirname(vod.hlsPlaylist)}/`
logger.debug(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
if (!tokenPath.startsWith('/')) {
throw new Error('tokenPath did not start with a forward slash');
}
if (!tokenPath.endsWith('/')) {
throw new Error('tokenPath did not end with a forward slash');
}
if (tokenPath.endsWith('//')) {
throw new Error('tokenPath ended with a double forward slash');
}
if (tokenPath.startsWith('//')) {
throw new Error('tokenPath started with a double forward slash');
}
const signedPlaylist = rewriteMp4ReferencesWithSignedUrls(
playlistContent,
tokenPath,
(path) =>
signUrl(`${env.CDN_ORIGIN}/${path}`, {
securityKey: env.CDN_TOKEN_SECRET!,
expirationTime: constants.timeUnits.oneDayInSeconds,
isDirectory: true,
pathAllowed: tokenPath,
})
)
return reply
.type('application/x-mpegURL')
.header('Cache-Control', 'public, max-age=60')
.send(signedPlaylist)
})
}