feat: support save live channel to favorite

This commit is contained in:
shinya
2025-08-26 16:36:44 +08:00
parent fa46e05c7c
commit 15b20603b8
12 changed files with 434 additions and 49 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
import { getConfig } from '@/lib/config';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from 'next/server';
import { getConfig } from '@/lib/config';
export const runtime = 'nodejs';
export async function GET(request: Request) {
+15 -1
View File
@@ -23,10 +23,13 @@ export async function GET(request: Request) {
}
const ua = liveSource.ua || 'AptvPlayer/1.4.10';
let response: Response | null = null;
let responseUsed = false;
try {
const decodedUrl = decodeURIComponent(url);
const response = await fetch(decodedUrl, {
response = await fetch(decodedUrl, {
cache: 'no-cache',
redirect: 'follow',
credentials: 'same-origin',
@@ -45,6 +48,7 @@ export async function GET(request: Request) {
// 获取最终的响应URL(处理重定向后的URL)
const finalUrl = response.url;
const m3u8Content = await response.text();
responseUsed = true; // 标记 response 已被使用
// 使用最终的响应URL作为baseUrl,而不是原始的请求URL
const baseUrl = getBaseUrl(finalUrl);
@@ -77,6 +81,16 @@ export async function GET(request: Request) {
});
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch m3u8' }, { status: 500 });
} finally {
// 确保 response 被正确关闭以释放资源
if (response && !responseUsed) {
try {
response.body?.cancel();
} catch (error) {
// 忽略关闭时的错误
console.warn('Failed to close response body:', error);
}
}
}
}
+94 -2
View File
@@ -21,9 +21,12 @@ export async function GET(request: Request) {
}
const ua = liveSource.ua || 'AptvPlayer/1.4.10';
let response: Response | null = null;
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
try {
const decodedUrl = decodeURIComponent(url);
const response = await fetch(decodedUrl, {
response = await fetch(decodedUrl, {
headers: {
'User-Agent': ua,
},
@@ -43,8 +46,97 @@ export async function GET(request: Request) {
if (contentLength) {
headers.set('Content-Length', contentLength);
}
return new Response(response.body, { headers });
// 使用流式传输,避免占用内存
const stream = new ReadableStream({
start(controller) {
if (!response?.body) {
controller.close();
return;
}
reader = response.body.getReader();
const isCancelled = false;
function pump() {
if (isCancelled || !reader) {
return;
}
reader.read().then(({ done, value }) => {
if (isCancelled) {
return;
}
if (done) {
controller.close();
cleanup();
return;
}
controller.enqueue(value);
pump();
}).catch((error) => {
if (!isCancelled) {
controller.error(error);
cleanup();
}
});
}
function cleanup() {
if (reader) {
try {
reader.releaseLock();
} catch (e) {
// reader 可能已经被释放,忽略错误
}
reader = null;
}
}
pump();
},
cancel() {
// 当流被取消时,确保释放所有资源
if (reader) {
try {
reader.releaseLock();
} catch (e) {
// reader 可能已经被释放,忽略错误
}
reader = null;
}
if (response?.body) {
try {
response.body.cancel();
} catch (e) {
// 忽略取消时的错误
}
}
}
});
return new Response(stream, { headers });
} catch (error) {
// 确保在错误情况下也释放资源
if (reader) {
try {
(reader as ReadableStreamDefaultReader<Uint8Array>).releaseLock();
} catch (e) {
// 忽略错误
}
}
if (response?.body) {
try {
response.body.cancel();
} catch (e) {
// 忽略错误
}
}
return NextResponse.json({ error: 'Failed to fetch segment' }, { status: 500 });
}
}