| |
| |
|
|
| import { getCorsHeaders, isDisallowedOrigin } from '../_cors.js'; |
| import { getRelayBaseUrl, getRelayHeaders } from '../_relay.js'; |
|
|
| export const config = { runtime: 'edge' }; |
|
|
| export default async function handler(request) { |
| const cors = getCorsHeaders(request); |
| if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: cors }); |
| if (isDisallowedOrigin(request)) { |
| return new Response(JSON.stringify({ error: 'Origin not allowed' }), { status: 403, headers: cors }); |
| } |
| const url = new URL(request.url); |
| const channel = url.searchParams.get('channel'); |
| const videoIdParam = url.searchParams.get('videoId'); |
|
|
| const params = new URLSearchParams(); |
| if (channel) params.set('channel', channel); |
| if (videoIdParam) params.set('videoId', videoIdParam); |
| const qs = params.toString(); |
|
|
| if (!qs) { |
| return new Response(JSON.stringify({ error: 'Missing channel or videoId parameter' }), { |
| status: 400, |
| headers: { ...cors, 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| |
| const relayBase = getRelayBaseUrl(); |
| if (relayBase) { |
| try { |
| const relayHeaders = getRelayHeaders({ 'User-Agent': 'WorldMonitor-Edge/1.0' }); |
| const relayRes = await fetch(`${relayBase}/youtube-live?${qs}`, { headers: relayHeaders }); |
| if (relayRes.ok) { |
| const data = await relayRes.json(); |
| const cacheTime = videoIdParam ? 3600 : 600; |
| return new Response(JSON.stringify(data), { |
| status: 200, |
| headers: { |
| ...cors, |
| 'Content-Type': 'application/json', |
| 'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}, stale-while-revalidate=60`, |
| }, |
| }); |
| } |
| } catch { } |
| } |
|
|
| |
| if (videoIdParam && /^[A-Za-z0-9_-]{11}$/.test(videoIdParam)) { |
| try { |
| const oembedRes = await fetch( |
| `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoIdParam}&format=json`, |
| { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }, |
| ); |
| if (oembedRes.ok) { |
| const data = await oembedRes.json(); |
| return new Response(JSON.stringify({ channelName: data.author_name || null, title: data.title || null, videoId: videoIdParam }), { |
| status: 200, |
| headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600' }, |
| }); |
| } |
| } catch { } |
| return new Response(JSON.stringify({ channelName: null, title: null, videoId: videoIdParam }), { |
| status: 200, |
| headers: { ...cors, 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| if (!channel) { |
| return new Response(JSON.stringify({ error: 'Missing channel parameter' }), { |
| status: 400, |
| headers: { ...cors, 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| |
| try { |
| const channelHandle = channel.startsWith('@') ? channel : `@${channel}`; |
| const response = await fetch(`https://www.youtube.com/${channelHandle}/live`, { |
| headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, |
| redirect: 'follow', |
| }); |
| if (!response.ok) { |
| return new Response(JSON.stringify({ videoId: null, channelExists: false }), { |
| status: 200, headers: { ...cors, 'Content-Type': 'application/json' }, |
| }); |
| } |
| const html = await response.text(); |
| const channelExists = html.includes('"channelId"') || html.includes('og:url'); |
| let channelName = null; |
| const ownerMatch = html.match(/"ownerChannelName"\s*:\s*"([^"]+)"/); |
| if (ownerMatch) channelName = ownerMatch[1]; |
| else { const am = html.match(/"author"\s*:\s*"([^"]+)"/); if (am) channelName = am[1]; } |
|
|
| let videoId = null; |
| const detailsIdx = html.indexOf('"videoDetails"'); |
| if (detailsIdx !== -1) { |
| const block = html.substring(detailsIdx, detailsIdx + 5000); |
| const vidMatch = block.match(/"videoId":"([a-zA-Z0-9_-]{11})"/); |
| const liveMatch = block.match(/"isLive"\s*:\s*true/); |
| if (vidMatch && liveMatch) videoId = vidMatch[1]; |
| } |
|
|
| let hlsUrl = null; |
| const hlsMatch = html.match(/"hlsManifestUrl"\s*:\s*"([^"]+)"/); |
| if (hlsMatch && videoId) hlsUrl = hlsMatch[1].replace(/\\u0026/g, '&'); |
|
|
| return new Response(JSON.stringify({ videoId, isLive: videoId !== null, channelExists, channelName, hlsUrl }), { |
| status: 200, |
| headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300, s-maxage=600, stale-while-revalidate=120' }, |
| }); |
| } catch { |
| return new Response(JSON.stringify({ videoId: null, error: 'Failed to fetch channel data' }), { |
| status: 200, headers: { ...cors, 'Content-Type': 'application/json' }, |
| }); |
| } |
| } |
|
|