From 7c1f1db0466f81e4de9a6f1d7bb7d74f65a0dfa1 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Thu, 25 Dec 2025 16:38:27 +0800 Subject: [PATCH] refactor: remove runtime fetching of environment subscriptions and `envSubscriptionsSet` state. --- README.md | 35 +++++++------------ app/api/config/route.ts | 8 ++--- app/settings/hooks/useSettingsPage.ts | 36 +++++-------------- app/settings/page.tsx | 3 -- components/settings/ImportModal.tsx | 24 ++----------- components/settings/SourceSettings.tsx | 18 ---------- lib/hooks/useSubscriptionSync.ts | 48 +++----------------------- lib/store/settings-store.ts | 39 ++++++++------------- 8 files changed, 45 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 1d6d4c1..f6dc10e 100644 --- a/README.md +++ b/README.md @@ -117,40 +117,29 @@ docker run -d -p 3000:3000 -e ACCESS_PASSWORD=your_secret_password --name kvideo - **无法在界面删除**:只能通过修改环境变量更改 - **与本地密码兼容**:两种密码都可以解锁应用 -## 🔄 自动订阅源配置 +## � 自动订阅源配置 -可以通过环境变量 `SUBSCRIPTION_SOURCES` 或 `NEXT_PUBLIC_SUBSCRIPTION_SOURCES` 自动配置订阅源。在 Docker 或 Cloudflare 部署中,这些变量在运行时生效,应用启动后会自动同步。 +可以通过环境变量 `NEXT_PUBLIC_SUBSCRIPTION_SOURCES` 自动配置订阅源,应用启动时会自动加载并设置为自动更新。 -### 支持格式 +**格式:** JSON 数组字符串,包含 `name` 和 `url` 字段。 -1. **JSON 数组字符串 (推荐):** 包含 `name` 和 `url` 字段。 - ```bash - SUBSCRIPTION_SOURCES='[{"name":"每日更新源","url":"https://example.com/api.json"}]' - ``` +**示例:** -2. **简单 URL (单个或多个):** 直接提供 URL,多个 URL 用逗号分隔。应用会自动生成默认名称。 - ```bash - SUBSCRIPTION_SOURCES='https://example.com/api.json,https://backup.com/api.json' - ``` - -### 部署示例 +```bash +NEXT_PUBLIC_SUBSCRIPTION_SOURCES='[{"name":"每日更新源","url":"https://example.com/api.json"},{"name":"备用源","url":"https://backup.com/api.json"}]' +``` **Docker 部署:** ```bash -docker run -d -p 3000:3000 \ - -e SUBSCRIPTION_SOURCES='https://example.com/api.json' \ - --name kvideo kuekhaoyang/kvideo:latest +docker run -d -p 3000:3000 -e NEXT_PUBLIC_SUBSCRIPTION_SOURCES='[{"name":"MySource","url":"..."}]' --name kvideo kuekhaoyang/kvideo:latest ``` -**Cloudflare / Vercel 部署:** +**Vercel 部署:** -在项目设置中添加环境变量: -- 变量名:`SUBSCRIPTION_SOURCES` -- 变量值:你的订阅地址或 JSON 字符串 - -> [!TIP] -> 现在应用支持**运行时配置**。在 Docker 中修改环境变量并重启容器即可生效,无需重新构建镜像。 +在 Vercel 项目设置中添加环境变量: +- 变量名:`NEXT_PUBLIC_SUBSCRIPTION_SOURCES` +- 变量值:`[{"name":"...","url":"..."}]` ## 📝 自定义源 JSON 格式 diff --git a/app/api/config/route.ts b/app/api/config/route.ts index b596f22..97a4772 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -5,23 +5,19 @@ import { NextRequest, NextResponse } from 'next/server'; -export const dynamic = 'force-dynamic'; export const runtime = 'edge'; -export async function GET() { - const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || ''; - const SUBSCRIPTION_SOURCES = process.env.SUBSCRIPTION_SOURCES || process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES || ''; +const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || ''; +export async function GET() { return NextResponse.json({ hasEnvPassword: ACCESS_PASSWORD.length > 0, - subscriptionSources: SUBSCRIPTION_SOURCES, }); } export async function POST(request: NextRequest) { try { const { password } = await request.json(); - const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || ''; if (!ACCESS_PASSWORD) { return NextResponse.json({ valid: false, message: 'No env password set' }); diff --git a/app/settings/hooks/useSettingsPage.ts b/app/settings/hooks/useSettingsPage.ts index 862e920..5bb7854 100644 --- a/app/settings/hooks/useSettingsPage.ts +++ b/app/settings/hooks/useSettingsPage.ts @@ -22,37 +22,20 @@ export function useSettingsPage() { const [passwordAccess, setPasswordAccess] = useState(false); const [accessPasswords, setAccessPasswords] = useState([]); const [envPasswordSet, setEnvPasswordSet] = useState(false); - const [envSubscriptionsSet, setEnvSubscriptionsSet] = useState(false); useEffect(() => { - const updateFromStore = () => { - const settings = settingsStore.getSettings(); - setSources(settings.sources || []); - setSubscriptions(settings.subscriptions || []); - setSortBy(settings.sortBy); - setPasswordAccess(settings.passwordAccess); - setAccessPasswords(settings.accessPasswords); - }; + const settings = settingsStore.getSettings(); + setSources(settings.sources || []); + setSubscriptions(settings.subscriptions || []); + setSortBy(settings.sortBy); + setPasswordAccess(settings.passwordAccess); + setAccessPasswords(settings.accessPasswords); - // Initial load - updateFromStore(); - - // Subscribe to changes - const unsubscribe = settingsStore.subscribe(updateFromStore); - - // Fetch env status + // Fetch env password status fetch('/api/config') .then(res => res.json()) - .then(data => { - setEnvPasswordSet(data.hasEnvPassword); - setEnvSubscriptionsSet(!!data.subscriptionSources); - }) - .catch(() => { - setEnvPasswordSet(false); - setEnvSubscriptionsSet(false); - }); - - return () => unsubscribe(); + .then(data => setEnvPasswordSet(data.hasEnvPassword)) + .catch(() => setEnvPasswordSet(false)); }, []); const handleSourcesChange = (newSources: VideoSource[]) => { @@ -291,7 +274,6 @@ export function useSettingsPage() { passwordAccess, accessPasswords, envPasswordSet, - envSubscriptionsSet, isAddModalOpen, isExportModalOpen, isImportModalOpen, diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 2049639..3df7ca6 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -38,7 +38,6 @@ export default function SettingsPage() { handleExport, handleImportFile, handleImportLink, - envSubscriptionsSet, subscriptions, handleAddSubscription, handleRemoveSubscription, @@ -76,7 +75,6 @@ export default function SettingsPage() { setIsAddModalOpen(true); }} onEditSource={handleEditSource} - envSubscriptionsSet={envSubscriptionsSet} /> {/* Sort Options */} @@ -120,7 +118,6 @@ export default function SettingsPage() { onAddSubscription={handleAddSubscription} onRemoveSubscription={handleRemoveSubscription} onRefreshSubscription={handleRefreshSubscription} - envSubscriptionsSet={envSubscriptionsSet} /> Promise | boolean; onRemoveSubscription: (id: string) => void; onRefreshSubscription: (sub: SourceSubscription) => Promise; - envSubscriptionsSet?: boolean; } -import { ShieldCheck } from 'lucide-react'; - export function ImportModal({ isOpen, onClose, @@ -34,8 +31,7 @@ export function ImportModal({ subscriptions, onAddSubscription, onRemoveSubscription, - onRefreshSubscription, - envSubscriptionsSet + onRefreshSubscription }: ImportModalProps) { const [activeTab, setActiveTab] = useState<'file' | 'link' | 'subscription'>('file'); @@ -55,8 +51,8 @@ export function ImportModal({ {/* Modal */}
@@ -77,20 +73,6 @@ export function ImportModal({ - {envSubscriptionsSet && ( -
- -
-

- 系统预设源已启用 -

-

- 通过环境变量设置,应用已自动同步最新预设源 -

-
-
- )} -
{activeTab === 'file' && ( diff --git a/components/settings/SourceSettings.tsx b/components/settings/SourceSettings.tsx index 0b59deb..e0e32f6 100644 --- a/components/settings/SourceSettings.tsx +++ b/components/settings/SourceSettings.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { ShieldCheck } from 'lucide-react'; import { SourceManager } from '@/components/settings/SourceManager'; import type { VideoSource } from '@/lib/types'; import { DEFAULT_SOURCES } from '@/lib/api/default-sources'; @@ -10,7 +9,6 @@ interface SourceSettingsProps { onRestoreDefaults: () => void; onAddSource: () => void; onEditSource?: (source: VideoSource) => void; - envSubscriptionsSet?: boolean; } export function SourceSettings({ @@ -19,7 +17,6 @@ export function SourceSettings({ onRestoreDefaults, onAddSource, onEditSource, - envSubscriptionsSet, }: SourceSettingsProps) { const [showAllSources, setShowAllSources] = useState(false); const [searchQuery, setSearchQuery] = useState(''); @@ -83,21 +80,6 @@ export function SourceSettings({ 管理视频来源,调整优先级和启用状态

- {/* Env Subscriptions Notice */} - {envSubscriptionsSet && ( -
- -
-

- 系统预设源已启用 -

-

- 发现环境变量配置,应用已自动同步最新预设源 -

-
-
- )} - {/* Search Bar */}
{ - let settings = settingsStore.getSettings(); - let anyChanged = false; - - // Fetch runtime config for subscription sources (to support Docker) - try { - const res = await fetch('/api/config'); - const config = await res.json(); - if (config.subscriptionSources) { - const runtimeSubs = parseEnvSubscriptions(config.subscriptionSources); - - // Merge runtime subscriptions if not already in settings - const currentSubs = [...settings.subscriptions]; - let subsAdded = false; - - runtimeSubs.forEach(rSub => { - const exists = currentSubs.some(s => s.url === rSub.url); - if (!exists) { - currentSubs.push({ - ...rSub, - autoRefresh: true - }); - subsAdded = true; - anyChanged = true; - } - }); - - if (subsAdded) { - settings = { - ...settings, - subscriptions: currentSubs - }; - } - } - } catch (e) { - console.error('Failed to fetch runtime config', e); - } - + const settings = settingsStore.getSettings(); const subscriptions = settings.subscriptions.filter(s => s.autoRefresh !== false); - if (subscriptions.length === 0) { - if (anyChanged) { - settingsStore.saveSettings(settings); - } - return; - } + if (subscriptions.length === 0) return; + let anyChanged = false; let currentSources = [...settings.sources]; let currentAdultSources = [...settings.adultSources]; let updatedSubscriptions = [...settings.subscriptions]; diff --git a/lib/store/settings-store.ts b/lib/store/settings-store.ts index 0d48cf8..9f77af6 100644 --- a/lib/store/settings-store.ts +++ b/lib/store/settings-store.ts @@ -44,14 +44,17 @@ export const getDefaultAdultSources = (): VideoSource[] => ADULT_SOURCES; -export function parseEnvSubscriptions(envValue: string | undefined): SourceSubscription[] { +function getEnvSubscriptions(): SourceSubscription[] { + if (typeof process === 'undefined' || !process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES) { + return []; + } + + const envValue = process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES.trim(); if (!envValue) return []; - const trimmed = envValue.trim(); - if (!trimmed) return []; // 1. Try JSON try { - const raw = JSON.parse(trimmed); + const raw = JSON.parse(envValue); if (Array.isArray(raw)) { return raw .filter((item: any) => item && typeof item.name === 'string' && typeof item.url === 'string') @@ -62,9 +65,11 @@ export function parseEnvSubscriptions(envValue: string | undefined): SourceSubsc } // 2. Try Simple URL (or comma separated) - if (trimmed.includes('http')) { - const urls = trimmed.split(',').map(u => u.trim()).filter(u => u.length > 0); + // Check if it looks like a URL (basic check) + if (envValue.includes('http')) { + const urls = envValue.split(',').map(u => u.trim()).filter(u => u.length > 0); return urls.map((url, index) => { + // Basic URL validation if (!url.startsWith('http')) return null; const name = urls.length > 1 @@ -77,14 +82,6 @@ export function parseEnvSubscriptions(envValue: string | undefined): SourceSubsc return []; } - -function getEnvSubscriptions(): SourceSubscription[] { - if (typeof process === 'undefined' || !process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES) { - return []; - } - - return parseEnvSubscriptions(process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES); -} // Debugging helper // console.log("Environment Subscriptions:", getEnvSubscriptions()); @@ -160,17 +157,11 @@ export const settingsStore = { }); // Filter out invalid sources (missing baseUrl etc) - const parsedSources = Array.isArray(parsed.sources) && parsed.sources.length > 0 - ? parsed.sources - : getDefaultSources(); + const validSources = (Array.isArray(parsed.sources) ? parsed.sources : getDefaultSources()) + .filter((s: any) => s && s.id && s.name && s.baseUrl); - const validSources = parsedSources.filter((s: any) => s && s.id && s.name && s.baseUrl); - - const parsedAdultSources = Array.isArray(parsed.adultSources) && parsed.adultSources.length > 0 - ? parsed.adultSources - : getDefaultAdultSources(); - - const validAdultSources = parsedAdultSources.filter((s: any) => s && s.id && s.name && s.baseUrl); + const validAdultSources = (Array.isArray(parsed.adultSources) ? parsed.adultSources : getDefaultAdultSources()) + .filter((s: any) => s && s.id && s.name && s.baseUrl); // Validate that parsed data has all required properties return {