增加tv mode开关
This commit is contained in:
@@ -424,6 +424,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能
|
|||||||
| NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true |
|
| NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true |
|
||||||
| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) |
|
| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) |
|
||||||
| NEXT_PUBLIC_DANMAKU_CACHE_EXPIRE_MINUTES | 弹幕缓存失效时间(分钟数,设为 0 时不缓存) | 0 或正整数 | 4320(3天) |
|
| NEXT_PUBLIC_DANMAKU_CACHE_EXPIRE_MINUTES | 弹幕缓存失效时间(分钟数,设为 0 时不缓存) | 0 或正整数 | 4320(3天) |
|
||||||
|
| ENABLE_TV_MODE | 是否启用 TV 模式;设为 false 后 /tv 不可访问,且不启动电视遥控 Socket.IO 监听 | true/false | true |
|
||||||
| ENABLE_TVBOX_SUBSCRIBE | 是否启用 TVBOX 订阅功能 | true/false | false |
|
| ENABLE_TVBOX_SUBSCRIBE | 是否启用 TVBOX 订阅功能 | true/false | false |
|
||||||
| TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) |
|
| TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) |
|
||||||
| TVBOX_BLOCKED_SOURCES | TVBOX 订阅屏蔽源列表(多个源用逗号分隔,匹配视频源的 key) | 逗号分隔的源 key | (空) |
|
| TVBOX_BLOCKED_SOURCES | TVBOX 订阅屏蔽源列表(多个源用逗号分隔,匹配视频源的 key) | 逗号分隔的源 key | (空) |
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ function shouldInitSQLite() {
|
|||||||
return process.env.NEXT_PUBLIC_STORAGE_TYPE === 'd1' && !isCloudflare && process.env.MOONTV_LITE !== 'true';
|
return process.env.NEXT_PUBLIC_STORAGE_TYPE === 'd1' && !isCloudflare && process.env.MOONTV_LITE !== 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTVModeEnabled() {
|
||||||
|
return process.env.ENABLE_TV_MODE !== 'false';
|
||||||
|
}
|
||||||
|
|
||||||
function ensureSQLiteReady() {
|
function ensureSQLiteReady() {
|
||||||
if (!shouldInitSQLite()) {
|
if (!shouldInitSQLite()) {
|
||||||
return;
|
return;
|
||||||
@@ -820,19 +824,30 @@ app.prepare().then(async () => {
|
|||||||
|
|
||||||
let watchRoomServer = null;
|
let watchRoomServer = null;
|
||||||
let tvRemoteServer = null;
|
let tvRemoteServer = null;
|
||||||
|
let io = null;
|
||||||
|
|
||||||
const io = new Server(httpServer, {
|
const tvModeEnabled = isTVModeEnabled();
|
||||||
path: '/socket.io',
|
const shouldStartInternalWatchRoom =
|
||||||
cors: {
|
watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal';
|
||||||
origin: '*',
|
|
||||||
methods: ['GET', 'POST'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
tvRemoteServer = new TVRemoteServer(io);
|
if (tvModeEnabled || shouldStartInternalWatchRoom) {
|
||||||
console.log('[TVRemote] Socket.IO remote server initialized');
|
io = new Server(httpServer, {
|
||||||
|
path: '/socket.io',
|
||||||
|
cors: {
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal') {
|
if (tvModeEnabled && io) {
|
||||||
|
tvRemoteServer = new TVRemoteServer(io);
|
||||||
|
console.log('[TVRemote] Socket.IO remote server initialized');
|
||||||
|
} else {
|
||||||
|
console.log('[TVRemote] TV mode disabled, remote server not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldStartInternalWatchRoom && io) {
|
||||||
// 初始化观影室服务器
|
// 初始化观影室服务器
|
||||||
watchRoomServer = new WatchRoomServer(io);
|
watchRoomServer = new WatchRoomServer(io);
|
||||||
console.log('[WatchRoom] Socket.IO server initialized');
|
console.log('[WatchRoom] Socket.IO server initialized');
|
||||||
@@ -851,7 +866,11 @@ app.prepare().then(async () => {
|
|||||||
})
|
})
|
||||||
.listen(port, () => {
|
.listen(port, () => {
|
||||||
console.log(`> Ready on http://${hostname}:${port}`);
|
console.log(`> Ready on http://${hostname}:${port}`);
|
||||||
console.log(`> Socket.IO ready on ws://${hostname}:${port}`);
|
if (io) {
|
||||||
|
console.log(`> Socket.IO ready on ws://${hostname}:${port}`);
|
||||||
|
} else {
|
||||||
|
console.log('> Socket.IO disabled');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const forceExit = (signal) => {
|
const forceExit = (signal) => {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export async function GET(request: NextRequest) {
|
|||||||
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus',
|
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus',
|
||||||
StorageType: 'localstorage',
|
StorageType: 'localstorage',
|
||||||
Version: CURRENT_VERSION,
|
Version: CURRENT_VERSION,
|
||||||
|
TVModeEnabled: process.env.ENABLE_TV_MODE !== 'false',
|
||||||
WatchRoom: watchRoomConfig,
|
WatchRoom: watchRoomConfig,
|
||||||
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
||||||
DanmakuAutoLoadDefault: true,
|
DanmakuAutoLoadDefault: true,
|
||||||
@@ -48,6 +49,7 @@ export async function GET(request: NextRequest) {
|
|||||||
SiteName: config.SiteConfig.SiteName,
|
SiteName: config.SiteConfig.SiteName,
|
||||||
StorageType: storageType,
|
StorageType: storageType,
|
||||||
Version: CURRENT_VERSION,
|
Version: CURRENT_VERSION,
|
||||||
|
TVModeEnabled: process.env.ENABLE_TV_MODE !== 'false',
|
||||||
WatchRoom: watchRoomConfig,
|
WatchRoom: watchRoomConfig,
|
||||||
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
||||||
EnableRegistration: config.SiteConfig.EnableRegistration || false,
|
EnableRegistration: config.SiteConfig.EnableRegistration || false,
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||||
|
|
||||||
const { listTVRemoteDevices } = require('@/lib/tv-remote-hub');
|
const { listTVRemoteDevices } = require('@/lib/tv-remote-hub');
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
if (!isTVModeEnabled()) {
|
||||||
|
return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo?.username) {
|
if (!authInfo?.username) {
|
||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||||
import type { TVRemoteKeyCommand } from '@/lib/tv-remote-types';
|
import type { TVRemoteKeyCommand } from '@/lib/tv-remote-types';
|
||||||
|
|
||||||
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
||||||
@@ -8,6 +9,10 @@ const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
if (!isTVModeEnabled()) {
|
||||||
|
return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo?.username) {
|
if (!authInfo?.username) {
|
||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||||
import type { TVRemoteTextCommand } from '@/lib/tv-remote-types';
|
import type { TVRemoteTextCommand } from '@/lib/tv-remote-types';
|
||||||
|
|
||||||
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
||||||
@@ -8,6 +9,10 @@ const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
|
if (!isTVModeEnabled()) {
|
||||||
|
return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
const authInfo = getAuthInfoFromCookie(request);
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
if (!authInfo?.username) {
|
if (!authInfo?.username) {
|
||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ export default async function RootLayout({
|
|||||||
BANGUMI_DATA_SOURCE: bangumiDataSource,
|
BANGUMI_DATA_SOURCE: bangumiDataSource,
|
||||||
BANGUMI_API_BASE_URL: bangumiApiBaseUrl,
|
BANGUMI_API_BASE_URL: bangumiApiBaseUrl,
|
||||||
BANGUMI_IMAGE_BASE_URL: bangumiImageBaseUrl,
|
BANGUMI_IMAGE_BASE_URL: bangumiImageBaseUrl,
|
||||||
|
ENABLE_TV_MODE: process.env.ENABLE_TV_MODE !== 'false',
|
||||||
ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true',
|
ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true',
|
||||||
ENABLE_OFFLINE_DOWNLOAD:
|
ENABLE_OFFLINE_DOWNLOAD:
|
||||||
process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import TVRemoteReceiver from '@/components/tv/TVRemoteReceiver';
|
import TVRemoteReceiver from '@/components/tv/TVRemoteReceiver';
|
||||||
|
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'TV - MoonTV Plus',
|
title: 'TV - MoonTV Plus',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
|
if (!isTVModeEnabled()) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
|
|
||||||
// 订阅相关状态
|
// 订阅相关状态
|
||||||
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
|
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
|
||||||
|
const [tvModeEnabled, setTvModeEnabled] = useState(true);
|
||||||
const [subscribeUrl, setSubscribeUrl] = useState('');
|
const [subscribeUrl, setSubscribeUrl] = useState('');
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
const [orionBaseUrlCopySuccess, setOrionBaseUrlCopySuccess] = useState(false);
|
const [orionBaseUrlCopySuccess, setOrionBaseUrlCopySuccess] = useState(false);
|
||||||
@@ -453,6 +454,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
const enabled =
|
const enabled =
|
||||||
(window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false;
|
(window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false;
|
||||||
setSubscribeEnabled(enabled);
|
setSubscribeEnabled(enabled);
|
||||||
|
setTvModeEnabled((window as any).RUNTIME_CONFIG?.ENABLE_TV_MODE !== false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -4295,14 +4297,22 @@ export const UserMenu: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='mt-5 text-sm leading-6 text-slate-600 dark:text-slate-400'>
|
<p className='mt-5 text-sm leading-6 text-slate-600 dark:text-slate-400'>
|
||||||
电视端打开 /tv 后会显示二维码;手机在这里打开相机扫描并确认登录。
|
{tvModeEnabled
|
||||||
|
? '电视端打开 /tv 后会显示二维码;手机在这里打开相机扫描并确认登录。'
|
||||||
|
: '当前部署未开启 TV 模式,/tv 页面和 Web 电视遥控不可用。'}
|
||||||
</p>
|
</p>
|
||||||
|
{!tvModeEnabled && (
|
||||||
|
<div className='mt-5 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm font-bold text-amber-800 dark:border-amber-300/20 dark:bg-amber-400/10 dark:text-amber-200'>
|
||||||
|
TV 模式未开启。请在环境变量中设置 ENABLE_TV_MODE=true 后重启服务。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='mt-5 grid gap-2 sm:grid-cols-2'>
|
<div className='mt-5 grid gap-2 sm:grid-cols-2'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={startTvQrScanner}
|
onClick={startTvQrScanner}
|
||||||
className='inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-rose-600 px-4 py-3 text-sm font-black text-white transition hover:bg-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/70'
|
disabled={!tvModeEnabled}
|
||||||
|
className='inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-rose-600 px-4 py-3 text-sm font-black text-white transition hover:bg-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/70 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 dark:disabled:bg-white/10 dark:disabled:text-slate-500'
|
||||||
>
|
>
|
||||||
打开相机扫码登录
|
打开相机扫码登录
|
||||||
<Smartphone className='h-4 w-4' />
|
<Smartphone className='h-4 w-4' />
|
||||||
@@ -4310,10 +4320,12 @@ export const UserMenu: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!tvModeEnabled) return;
|
||||||
setIsSubscribeOpen(false);
|
setIsSubscribeOpen(false);
|
||||||
setIsTVRemoteOpen(true);
|
setIsTVRemoteOpen(true);
|
||||||
}}
|
}}
|
||||||
className='inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-slate-950 px-4 py-3 text-sm font-black text-white transition hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500/70 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200'
|
disabled={!tvModeEnabled}
|
||||||
|
className='inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-slate-950 px-4 py-3 text-sm font-black text-white transition hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500/70 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200 dark:disabled:bg-white/10 dark:disabled:text-slate-500'
|
||||||
>
|
>
|
||||||
<Sliders className='h-4 w-4' />
|
<Sliders className='h-4 w-4' />
|
||||||
远程电视遥控器
|
远程电视遥控器
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isTVModeEnabled() {
|
||||||
|
return process.env.ENABLE_TV_MODE !== 'false';
|
||||||
|
}
|
||||||
@@ -4,10 +4,15 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { TOKEN_CONFIG } from '@/lib/refresh-token';
|
import { TOKEN_CONFIG } from '@/lib/refresh-token';
|
||||||
|
import { isTVModeEnabled } from '@/lib/tv-mode';
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
if (!isTVModeEnabled() && isTVModePath(pathname)) {
|
||||||
|
return new NextResponse('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
// 跳过不需要认证的路径
|
// 跳过不需要认证的路径
|
||||||
if (shouldSkipAuth(pathname)) {
|
if (shouldSkipAuth(pathname)) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -171,6 +176,10 @@ function shouldSkipAuth(pathname: string): boolean {
|
|||||||
return skipPaths.some((path) => pathname.startsWith(path));
|
return skipPaths.some((path) => pathname.startsWith(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTVModePath(pathname: string): boolean {
|
||||||
|
return pathname === '/tv' || pathname.startsWith('/tv/') || pathname.startsWith('/api/tv-remote/');
|
||||||
|
}
|
||||||
|
|
||||||
// 配置middleware匹配规则
|
// 配置middleware匹配规则
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
|
|||||||
Reference in New Issue
Block a user