mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-21 19:53:43 +08:00
feat: Implement user-defined video sources and danmaku API management with new settings pages and a dedicated store.
This commit is contained in:
@@ -16,6 +16,7 @@ const effectiveAdminPassword = ADMIN_PASSWORD || ACCESS_PASSWORD;
|
||||
interface AccountInfo {
|
||||
name: string;
|
||||
role: 'super_admin' | 'admin' | 'viewer';
|
||||
customPermissions?: string[];
|
||||
}
|
||||
|
||||
function getAccountList(): AccountInfo[] {
|
||||
@@ -37,8 +38,12 @@ function getAccountList(): AccountInfo[] {
|
||||
const name = parts[1].trim();
|
||||
const parsedRole = parts[2]?.trim();
|
||||
const role = parsedRole === 'super_admin' ? 'super_admin' : parsedRole === 'admin' ? 'admin' : 'viewer';
|
||||
const perms = parts[3]?.trim();
|
||||
const customPermissions = perms
|
||||
? perms.split('|').map(p => p.trim()).filter(p => p.length > 0)
|
||||
: undefined;
|
||||
if (name) {
|
||||
accounts.push({ name, role });
|
||||
accounts.push({ name, role, ...(customPermissions && customPermissions.length > 0 ? { customPermissions } : {}) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ interface AccountEntry {
|
||||
password: string;
|
||||
name: string;
|
||||
role: 'super_admin' | 'admin' | 'viewer';
|
||||
customPermissions: string[];
|
||||
}
|
||||
|
||||
function parseAccounts(): AccountEntry[] {
|
||||
@@ -31,12 +32,16 @@ function parseAccounts(): AccountEntry[] {
|
||||
.map(entry => {
|
||||
const parts = entry.split(':');
|
||||
if (parts.length < 2) return null;
|
||||
const [password, name, role] = parts;
|
||||
const [password, name, role, perms] = parts;
|
||||
const parsedRole = role?.trim();
|
||||
const customPermissions = perms
|
||||
? perms.split('|').map(p => p.trim()).filter(p => p.length > 0)
|
||||
: [];
|
||||
return {
|
||||
password: password.trim(),
|
||||
name: name.trim(),
|
||||
role: (parsedRole === 'super_admin' ? 'super_admin' : parsedRole === 'admin' ? 'admin' : 'viewer') as 'super_admin' | 'admin' | 'viewer',
|
||||
customPermissions,
|
||||
};
|
||||
})
|
||||
.filter((a): a is AccountEntry => a !== null && a.password.length > 0 && a.name.length > 0);
|
||||
@@ -96,6 +101,7 @@ export async function POST(request: NextRequest) {
|
||||
role: account.role,
|
||||
profileId,
|
||||
persistSession: PERSIST_SESSION,
|
||||
customPermissions: account.customPermissions.length > 0 ? account.customPermissions : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ function rewriteM3u8(content: string, baseUrl: string, proxyBase: string): strin
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const url = request.nextUrl.searchParams.get('url');
|
||||
const customUa = request.nextUrl.searchParams.get('ua');
|
||||
const customReferer = request.nextUrl.searchParams.get('referer');
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 });
|
||||
@@ -54,9 +56,9 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
const fetchHeaders: Record<string, string> = {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; KVideo/1.0)',
|
||||
'User-Agent': customUa || 'Mozilla/5.0 (compatible; KVideo/1.0)',
|
||||
'Accept': '*/*',
|
||||
'Referer': `${parsedUrl.protocol}//${parsedUrl.host}/`,
|
||||
'Referer': customReferer || `${parsedUrl.protocol}//${parsedUrl.host}/`,
|
||||
'Origin': `${parsedUrl.protocol}//${parsedUrl.host}`,
|
||||
};
|
||||
|
||||
@@ -66,7 +68,15 @@ export async function GET(request: NextRequest) {
|
||||
fetchHeaders['Range'] = rangeHeader;
|
||||
}
|
||||
|
||||
const response = await fetch(url, { headers: fetchHeaders });
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: fetchHeaders,
|
||||
redirect: 'follow',
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
@@ -76,10 +86,44 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const isM3u8 = url.includes('.m3u8') ||
|
||||
let isM3u8 = url.includes('.m3u8') ||
|
||||
contentType.includes('mpegurl') ||
|
||||
contentType.includes('x-mpegURL');
|
||||
|
||||
// If content-type is ambiguous, check the response body for M3U header
|
||||
if (!isM3u8 && (contentType.includes('text/plain') || contentType.includes('application/octet-stream') || !contentType)) {
|
||||
const text = await response.text();
|
||||
if (text.trimStart().startsWith('#EXTM3U') || text.trimStart().startsWith('#EXT-X-')) {
|
||||
isM3u8 = true;
|
||||
}
|
||||
// For detected M3U8 from body check, process inline
|
||||
if (isM3u8) {
|
||||
const proxyBase = `/api/iptv/stream?${customUa ? `ua=${encodeURIComponent(customUa)}&` : ''}${customReferer ? `referer=${encodeURIComponent(customReferer)}&` : ''}url=`;
|
||||
const rewritten = rewriteM3u8(text, url, proxyBase);
|
||||
return new NextResponse(rewritten, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.apple.mpegurl',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
// Not M3U8, return original text as binary-like response
|
||||
return new NextResponse(text, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': contentType || 'video/mp2t',
|
||||
'Cache-Control': 'public, max-age=60',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
@@ -89,7 +133,7 @@ export async function GET(request: NextRequest) {
|
||||
if (isM3u8) {
|
||||
// Parse and rewrite manifest
|
||||
const text = await response.text();
|
||||
const proxyBase = `/api/iptv/stream?url=`;
|
||||
const proxyBase = `/api/iptv/stream?${customUa ? `ua=${encodeURIComponent(customUa)}&` : ''}${customReferer ? `referer=${encodeURIComponent(customReferer)}&` : ''}url=`;
|
||||
const rewritten = rewriteM3u8(text, url, proxyBase);
|
||||
|
||||
return new NextResponse(rewritten, {
|
||||
|
||||
Reference in New Issue
Block a user