From a6829989309932db58c3fb45ed695f2abf04d981 Mon Sep 17 00:00:00 2001
From: mtvpls
Date: Mon, 1 Jun 2026 02:05:05 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0tv=20mode=E5=BC=80=E5=85=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 1 +
server.js | 41 +++++++++++++++++++-------
src/app/api/server-config/route.ts | 2 ++
src/app/api/tv-remote/devices/route.ts | 5 ++++
src/app/api/tv-remote/key/route.ts | 5 ++++
src/app/api/tv-remote/text/route.ts | 5 ++++
src/app/layout.tsx | 1 +
src/app/tv/layout.tsx | 6 ++++
src/components/UserMenu.tsx | 20 ++++++++++---
src/lib/tv-mode.ts | 3 ++
src/middleware.ts | 9 ++++++
11 files changed, 83 insertions(+), 15 deletions(-)
create mode 100644 src/lib/tv-mode.ts
diff --git a/README.md b/README.md
index 2cca7d2..8c18abd 100644
--- a/README.md
+++ b/README.md
@@ -424,6 +424,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能
| NEXT_PUBLIC_FLUID_SEARCH | 是否开启搜索接口流式输出 | true/ false | true |
| NEXT_PUBLIC_PROXY_M3U8_TOKEN | M3U8 代理 API 鉴权 Token(外部播放器跳转时的鉴权token,不填为无鉴权) | 任意字符串 | (空) |
| 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 |
| TVBOX_SUBSCRIBE_TOKEN | TVBOX 订阅 API 访问 Token,如启用TVBOX功能必须设置该项 | 任意字符串 | (空) |
| TVBOX_BLOCKED_SOURCES | TVBOX 订阅屏蔽源列表(多个源用逗号分隔,匹配视频源的 key) | 逗号分隔的源 key | (空) |
diff --git a/server.js b/server.js
index c0de442..6196be4 100644
--- a/server.js
+++ b/server.js
@@ -17,6 +17,10 @@ function shouldInitSQLite() {
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() {
if (!shouldInitSQLite()) {
return;
@@ -820,19 +824,30 @@ app.prepare().then(async () => {
let watchRoomServer = null;
let tvRemoteServer = null;
+ let io = null;
- const io = new Server(httpServer, {
- path: '/socket.io',
- cors: {
- origin: '*',
- methods: ['GET', 'POST'],
- },
- });
+ const tvModeEnabled = isTVModeEnabled();
+ const shouldStartInternalWatchRoom =
+ watchRoomConfig.enabled && watchRoomConfig.serverType === 'internal';
- tvRemoteServer = new TVRemoteServer(io);
- console.log('[TVRemote] Socket.IO remote server initialized');
+ if (tvModeEnabled || shouldStartInternalWatchRoom) {
+ 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);
console.log('[WatchRoom] Socket.IO server initialized');
@@ -851,7 +866,11 @@ app.prepare().then(async () => {
})
.listen(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) => {
diff --git a/src/app/api/server-config/route.ts b/src/app/api/server-config/route.ts
index eacf716..a87f63a 100644
--- a/src/app/api/server-config/route.ts
+++ b/src/app/api/server-config/route.ts
@@ -36,6 +36,7 @@ export async function GET(request: NextRequest) {
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus',
StorageType: 'localstorage',
Version: CURRENT_VERSION,
+ TVModeEnabled: process.env.ENABLE_TV_MODE !== 'false',
WatchRoom: watchRoomConfig,
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
DanmakuAutoLoadDefault: true,
@@ -48,6 +49,7 @@ export async function GET(request: NextRequest) {
SiteName: config.SiteConfig.SiteName,
StorageType: storageType,
Version: CURRENT_VERSION,
+ TVModeEnabled: process.env.ENABLE_TV_MODE !== 'false',
WatchRoom: watchRoomConfig,
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
EnableRegistration: config.SiteConfig.EnableRegistration || false,
diff --git a/src/app/api/tv-remote/devices/route.ts b/src/app/api/tv-remote/devices/route.ts
index a260da6..799d6ec 100644
--- a/src/app/api/tv-remote/devices/route.ts
+++ b/src/app/api/tv-remote/devices/route.ts
@@ -1,12 +1,17 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
+import { isTVModeEnabled } from '@/lib/tv-mode';
const { listTVRemoteDevices } = require('@/lib/tv-remote-hub');
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
+ if (!isTVModeEnabled()) {
+ return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
+ }
+
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
diff --git a/src/app/api/tv-remote/key/route.ts b/src/app/api/tv-remote/key/route.ts
index 4b4e11c..1e88d58 100644
--- a/src/app/api/tv-remote/key/route.ts
+++ b/src/app/api/tv-remote/key/route.ts
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
+import { isTVModeEnabled } from '@/lib/tv-mode';
import type { TVRemoteKeyCommand } from '@/lib/tv-remote-types';
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
@@ -8,6 +9,10 @@ const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
+ if (!isTVModeEnabled()) {
+ return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
+ }
+
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
diff --git a/src/app/api/tv-remote/text/route.ts b/src/app/api/tv-remote/text/route.ts
index 97525da..4b0b02d 100644
--- a/src/app/api/tv-remote/text/route.ts
+++ b/src/app/api/tv-remote/text/route.ts
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
+import { isTVModeEnabled } from '@/lib/tv-mode';
import type { TVRemoteTextCommand } from '@/lib/tv-remote-types';
const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
@@ -8,6 +9,10 @@ const { sendTVRemoteCommand } = require('@/lib/tv-remote-hub');
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
+ if (!isTVModeEnabled()) {
+ return NextResponse.json({ error: 'TV 模式未启用' }, { status: 404 });
+ }
+
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: '未登录' }, { status: 401 });
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 7b4a74f..2e6d1b4 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -255,6 +255,7 @@ export default async function RootLayout({
BANGUMI_DATA_SOURCE: bangumiDataSource,
BANGUMI_API_BASE_URL: bangumiApiBaseUrl,
BANGUMI_IMAGE_BASE_URL: bangumiImageBaseUrl,
+ ENABLE_TV_MODE: process.env.ENABLE_TV_MODE !== 'false',
ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true',
ENABLE_OFFLINE_DOWNLOAD:
process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
diff --git a/src/app/tv/layout.tsx b/src/app/tv/layout.tsx
index f362818..4b79496 100644
--- a/src/app/tv/layout.tsx
+++ b/src/app/tv/layout.tsx
@@ -1,12 +1,18 @@
import { ReactNode } from 'react';
+import { notFound } from 'next/navigation';
import TVRemoteReceiver from '@/components/tv/TVRemoteReceiver';
+import { isTVModeEnabled } from '@/lib/tv-mode';
export const metadata = {
title: 'TV - MoonTV Plus',
};
export default function Layout({ children }: { children: ReactNode }) {
+ if (!isTVModeEnabled()) {
+ notFound();
+ }
+
return (
<>
{children}
diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx
index 2fd53d1..dcab099 100644
--- a/src/components/UserMenu.tsx
+++ b/src/components/UserMenu.tsx
@@ -87,6 +87,7 @@ export const UserMenu: React.FC = () => {
// 订阅相关状态
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
+ const [tvModeEnabled, setTvModeEnabled] = useState(true);
const [subscribeUrl, setSubscribeUrl] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const [orionBaseUrlCopySuccess, setOrionBaseUrlCopySuccess] = useState(false);
@@ -453,6 +454,7 @@ export const UserMenu: React.FC = () => {
const enabled =
(window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false;
setSubscribeEnabled(enabled);
+ setTvModeEnabled((window as any).RUNTIME_CONFIG?.ENABLE_TV_MODE !== false);
}
}, []);
@@ -4295,14 +4297,22 @@ export const UserMenu: React.FC = () => {
-
- 电视端打开 /tv 后会显示二维码;手机在这里打开相机扫描并确认登录。
+
+ {tvModeEnabled
+ ? '电视端打开 /tv 后会显示二维码;手机在这里打开相机扫描并确认登录。'
+ : '当前部署未开启 TV 模式,/tv 页面和 Web 电视遥控不可用。'}
+ {!tvModeEnabled && (
+
+ TV 模式未开启。请在环境变量中设置 ENABLE_TV_MODE=true 后重启服务。
+
+ )}