refactor strm generate section

This commit is contained in:
ShukeBta
2026-06-27 15:14:24 +08:00
parent d45a8ceeec
commit f88b833cbc
2 changed files with 280 additions and 168 deletions
+21 -168
View File
@@ -1,11 +1,18 @@
import type { FormEvent } from 'react'
import { Loader2, Save, Wand2 } from 'lucide-react'
import type { GenerateSTRMResult } from '../api/strm'
import type { Library } from '../types'
import { currentOrigin, type CloudPlaybackMode } from './strmPageModel'
import type { CloudPlaybackMode } from './strmPageModel'
import {
PlaybackPreferencePanel,
PlaybackTogglePanel,
StrmGenerateForm,
StrmGenerateHeader,
StrmGenerateHint,
StrmGenerateResultPanel,
} from './StrmGenerateSectionParts'
type StrmGenerateSectionProps = {
export type StrmGenerateSectionProps = {
libraries: Library[]
generateLibraryID: string
baseURL: string
@@ -31,173 +38,19 @@ type StrmGenerateSectionProps = {
setOverwrite: (value: boolean) => void
}
export function StrmGenerateSection({
libraries,
generateLibraryID,
baseURL,
outputDir,
cloudPlaybackMode,
strmPlaybackEnabled,
redirectProxyEnabled,
autoGenerate,
savingSettings,
overwrite,
generating,
generateResult,
playbackStatus,
onGenerate,
saveSTRMSettings,
setGenerateLibraryID,
setBaseURL,
setOutputDir,
setCloudPlaybackMode,
setStrmPlaybackEnabled,
setRedirectProxyEnabled,
setAutoGenerate,
setOverwrite,
}: StrmGenerateSectionProps) {
export function StrmGenerateSection(props: StrmGenerateSectionProps) {
return (
<section className="glass-panel space-y-4">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="font-display text-lg font-semibold text-ink-600">自动生成 STRM 文件</h2>
<p className="text-sm text-ink-50">
只需要填写自己的访问域名,系统会按媒体库内每个媒体批量生成可播放的 .strm 文件。
</p>
</div>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${
strmPlaybackEnabled || redirectProxyEnabled
? 'border-emerald-300/40 bg-emerald-400/10 text-emerald-500'
: 'border-red-300/40 bg-red-400/10 text-red-500'
}`}>
{playbackStatus}
</span>
</div>
<div className="grid gap-3 rounded-2xl border border-gray-200 bg-white/70 p-4 md:grid-cols-2">
<label className="flex items-start gap-3 text-sm text-ink-100">
<input
type="checkbox"
className="mt-1 h-4 w-4 accent-primary-400"
checked={strmPlaybackEnabled}
onChange={(e) => setStrmPlaybackEnabled(e.target.checked)}
/>
<span>
<span className="block font-medium text-ink-600">启用 STRMURL 播放</span>
<span className="text-xs text-ink-50">第三方客户端可拿到 /api/stream/媒体ID 入口,适合 STRM 管理和自动生成方案。</span>
</span>
</label>
<label className="flex items-start gap-3 text-sm text-ink-100">
<input
type="checkbox"
className="mt-1 h-4 w-4 accent-primary-400"
checked={redirectProxyEnabled}
onChange={(e) => setRedirectProxyEnabled(e.target.checked)}
/>
<span>
<span className="block font-medium text-ink-600">启用 302/反代播放</span>
<span className="text-xs text-ink-50">第三方客户端可拿到 /Videos/媒体ID/stream 入口,由服务端解析后 302 或必要时反代。</span>
</span>
</label>
</div>
<div className="grid gap-3 rounded-2xl border border-gray-200 bg-white/70 p-4 md:grid-cols-[1fr_1fr_auto]">
<label className="text-sm text-ink-100">
<span className="mb-1 block font-medium text-ink-600">两者都开启时优先</span>
<select
className="input-base"
value={cloudPlaybackMode}
onChange={(e) => setCloudPlaybackMode(e.target.value as CloudPlaybackMode)}
disabled={!strmPlaybackEnabled || !redirectProxyEnabled}
>
<option value="strm">优先 STRMURL</option>
<option value="redirect_proxy">优先 302/反代</option>
</select>
<span className="mt-1 block text-xs text-ink-50">只开启一个时自动使用已开启的播放方式;两个都关闭时云盘媒体不向第三方提供播放。</span>
</label>
<label className="flex items-start gap-3 text-sm text-ink-100">
<input
type="checkbox"
className="mt-1 h-4 w-4 accent-primary-400"
checked={autoGenerate}
onChange={(e) => setAutoGenerate(e.target.checked)}
/>
<span>
<span className="block font-medium text-ink-600">扫描后自动刷新 STRM 文件</span>
<span className="text-xs text-ink-50">默认关闭,避免扫描大型网盘库时重复写文件。</span>
</span>
</label>
<button type="button" className="neon-button self-center" disabled={savingSettings} onClick={saveSTRMSettings}>
{savingSettings ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
保存开关
</button>
</div>
<form onSubmit={onGenerate} className="grid gap-3 md:grid-cols-4">
<select
required
className="input-base"
value={generateLibraryID}
onChange={(e) => setGenerateLibraryID(e.target.value)}
>
<option value="" disabled>
选择媒体库
</option>
<option value="*">全部媒体库</option>
{libraries.map((library) => (
<option key={library.id} value={library.id}>
{library.name} ({library.type})
</option>
))}
</select>
<input
required
className="input-base md:col-span-2"
placeholder="http://NAS-IP:18080 或 https://media.example.com"
value={baseURL}
onChange={(e) => setBaseURL(e.target.value)}
/>
<button
type="button"
className="rounded-2xl border border-primary-400/40 px-3 py-2 text-sm text-brand-500 transition hover:bg-primary-400/10"
onClick={() => setBaseURL(currentOrigin())}
>
使用当前访问地址
</button>
<label className="flex items-center gap-2 rounded-2xl border border-gray-200 bg-white/70 px-3 py-2 text-sm text-ink-50">
<input
type="checkbox"
checked={overwrite}
onChange={(e) => setOverwrite(e.target.checked)}
/>
覆盖已存在
</label>
<input
className="input-base md:col-span-3"
placeholder="输出目录可留空,默认写入 data/strm/分类/子分类"
value={outputDir}
onChange={(e) => setOutputDir(e.target.value)}
/>
<button type="submit" disabled={generating || !generateLibraryID || !baseURL.trim()} className="neon-button md:col-span-4">
{generating ? <Loader2 size={16} className="animate-spin" /> : <Wand2 size={16} />}
{generating ? '生成中…' : '批量生成 STRM'}
</button>
</form>
<p className="text-xs text-sand-500">
生成内容为 <code>域名 + /api/stream/媒体ID?token=...</code>;第三方客户端播放优先方式由上方「STRMURL / 302反代」模式决定。域名会同步保存到系统设置中的「公开访问域名 / STRM 域名」。
</p>
{generateResult && (
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 text-sm text-ink-50">
<div className="font-semibold text-ink-600">
输出目录:{generateResult.output_dir}
</div>
<div className="mt-1">
新增 {generateResult.generated} · 更新 {generateResult.updated} · 跳过 {generateResult.skipped} · 清理 {generateResult.cleaned || 0}
</div>
{generateResult.errors && generateResult.errors.length > 0 && (
<div className="mt-2 text-red-500">
失败 {generateResult.errors.length} 条:{generateResult.errors.slice(0, 3).join(';')}
</div>
)}
</div>
)}
<StrmGenerateHeader
playbackStatus={props.playbackStatus}
strmPlaybackEnabled={props.strmPlaybackEnabled}
redirectProxyEnabled={props.redirectProxyEnabled}
/>
<PlaybackTogglePanel {...props} />
<PlaybackPreferencePanel {...props} />
<StrmGenerateForm {...props} />
<StrmGenerateHint />
<StrmGenerateResultPanel result={props.generateResult} />
</section>
)
}
+259
View File
@@ -0,0 +1,259 @@
import { Loader2, Save, Wand2 } from 'lucide-react'
import type { GenerateSTRMResult } from '../api/strm'
import type { Library } from '../types'
import { currentOrigin, type CloudPlaybackMode } from './strmPageModel'
import type { StrmGenerateSectionProps } from './StrmGenerateSection'
type PlaybackStatusProps = Pick<
StrmGenerateSectionProps,
'playbackStatus' | 'strmPlaybackEnabled' | 'redirectProxyEnabled'
>
export function StrmGenerateHeader({ playbackStatus, strmPlaybackEnabled, redirectProxyEnabled }: PlaybackStatusProps) {
const enabled = strmPlaybackEnabled || redirectProxyEnabled
return (
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="font-display text-lg font-semibold text-ink-600">自动生成 STRM 文件</h2>
<p className="text-sm text-ink-50">
只需要填写自己的访问域名,系统会按媒体库内每个媒体批量生成可播放的 .strm 文件。
</p>
</div>
<span className={`rounded-full border px-3 py-1 text-xs font-semibold ${
enabled
? 'border-emerald-300/40 bg-emerald-400/10 text-emerald-500'
: 'border-red-300/40 bg-red-400/10 text-red-500'
}`}>
{playbackStatus}
</span>
</div>
)
}
type PlaybackToggleProps = Pick<
StrmGenerateSectionProps,
'strmPlaybackEnabled' | 'redirectProxyEnabled' | 'setStrmPlaybackEnabled' | 'setRedirectProxyEnabled'
>
export function PlaybackTogglePanel({
strmPlaybackEnabled,
redirectProxyEnabled,
setStrmPlaybackEnabled,
setRedirectProxyEnabled,
}: PlaybackToggleProps) {
return (
<div className="grid gap-3 rounded-2xl border border-gray-200 bg-white/70 p-4 md:grid-cols-2">
<PlaybackToggle
checked={strmPlaybackEnabled}
title="启用 STRMURL 播放"
description="第三方客户端可拿到 /api/stream/媒体ID 入口,适合 STRM 管理和自动生成方案。"
onChange={setStrmPlaybackEnabled}
/>
<PlaybackToggle
checked={redirectProxyEnabled}
title="启用 302/反代播放"
description="第三方客户端可拿到 /Videos/媒体ID/stream 入口,由服务端解析后 302 或必要时反代。"
onChange={setRedirectProxyEnabled}
/>
</div>
)
}
type PlaybackToggleItemProps = {
checked: boolean
title: string
description: string
onChange: (value: boolean) => void
}
function PlaybackToggle({ checked, title, description, onChange }: PlaybackToggleItemProps) {
return (
<label className="flex items-start gap-3 text-sm text-ink-100">
<input
type="checkbox"
className="mt-1 h-4 w-4 accent-primary-400"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
<span>
<span className="block font-medium text-ink-600">{title}</span>
<span className="text-xs text-ink-50">{description}</span>
</span>
</label>
)
}
type PlaybackPreferenceProps = Pick<
StrmGenerateSectionProps,
| 'cloudPlaybackMode'
| 'strmPlaybackEnabled'
| 'redirectProxyEnabled'
| 'autoGenerate'
| 'savingSettings'
| 'saveSTRMSettings'
| 'setCloudPlaybackMode'
| 'setAutoGenerate'
>
export function PlaybackPreferencePanel({
cloudPlaybackMode,
strmPlaybackEnabled,
redirectProxyEnabled,
autoGenerate,
savingSettings,
saveSTRMSettings,
setCloudPlaybackMode,
setAutoGenerate,
}: PlaybackPreferenceProps) {
return (
<div className="grid gap-3 rounded-2xl border border-gray-200 bg-white/70 p-4 md:grid-cols-[1fr_1fr_auto]">
<label className="text-sm text-ink-100">
<span className="mb-1 block font-medium text-ink-600">两者都开启时优先</span>
<select
className="input-base"
value={cloudPlaybackMode}
onChange={(e) => setCloudPlaybackMode(e.target.value as CloudPlaybackMode)}
disabled={!strmPlaybackEnabled || !redirectProxyEnabled}
>
<option value="strm">优先 STRMURL</option>
<option value="redirect_proxy">优先 302/反代</option>
</select>
<span className="mt-1 block text-xs text-ink-50">只开启一个时自动使用已开启的播放方式;两个都关闭时云盘媒体不向第三方提供播放。</span>
</label>
<PlaybackToggle
checked={autoGenerate}
title="扫描后自动刷新 STRM 文件"
description="默认关闭,避免扫描大型网盘库时重复写文件。"
onChange={setAutoGenerate}
/>
<button type="button" className="neon-button self-center" disabled={savingSettings} onClick={saveSTRMSettings}>
{savingSettings ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
保存开关
</button>
</div>
)
}
type StrmGenerateFormProps = Pick<
StrmGenerateSectionProps,
| 'libraries'
| 'generateLibraryID'
| 'baseURL'
| 'outputDir'
| 'overwrite'
| 'generating'
| 'onGenerate'
| 'setGenerateLibraryID'
| 'setBaseURL'
| 'setOutputDir'
| 'setOverwrite'
>
export function StrmGenerateForm({
libraries,
generateLibraryID,
baseURL,
outputDir,
overwrite,
generating,
onGenerate,
setGenerateLibraryID,
setBaseURL,
setOutputDir,
setOverwrite,
}: StrmGenerateFormProps) {
return (
<form onSubmit={onGenerate} className="grid gap-3 md:grid-cols-4">
<LibrarySelect libraries={libraries} value={generateLibraryID} onChange={setGenerateLibraryID} />
<input
required
className="input-base md:col-span-2"
placeholder="http://NAS-IP:18080 或 https://media.example.com"
value={baseURL}
onChange={(e) => setBaseURL(e.target.value)}
/>
<button
type="button"
className="rounded-2xl border border-primary-400/40 px-3 py-2 text-sm text-brand-500 transition hover:bg-primary-400/10"
onClick={() => setBaseURL(currentOrigin())}
>
使用当前访问地址
</button>
<label className="flex items-center gap-2 rounded-2xl border border-gray-200 bg-white/70 px-3 py-2 text-sm text-ink-50">
<input
type="checkbox"
checked={overwrite}
onChange={(e) => setOverwrite(e.target.checked)}
/>
覆盖已存在
</label>
<input
className="input-base md:col-span-3"
placeholder="输出目录可留空,默认写入 data/strm/分类/子分类"
value={outputDir}
onChange={(e) => setOutputDir(e.target.value)}
/>
<button type="submit" disabled={generating || !generateLibraryID || !baseURL.trim()} className="neon-button md:col-span-4">
{generating ? <Loader2 size={16} className="animate-spin" /> : <Wand2 size={16} />}
{generating ? '生成中…' : '批量生成 STRM'}
</button>
</form>
)
}
type LibrarySelectProps = {
libraries: Library[]
value: string
onChange: (value: string) => void
}
function LibrarySelect({ libraries, value, onChange }: LibrarySelectProps) {
return (
<select
required
className="input-base"
value={value}
onChange={(e) => onChange(e.target.value)}
>
<option value="" disabled>
选择媒体库
</option>
<option value="*">全部媒体库</option>
{libraries.map((library) => (
<option key={library.id} value={library.id}>
{library.name} ({library.type})
</option>
))}
</select>
)
}
export function StrmGenerateHint() {
return (
<p className="text-xs text-sand-500">
生成内容为 <code>域名 + /api/stream/媒体ID?token=...</code>;第三方客户端播放优先方式由上方「STRMURL / 302反代」模式决定。域名会同步保存到系统设置中的「公开访问域名 / STRM 域名」。
</p>
)
}
export function StrmGenerateResultPanel({ result }: { result: GenerateSTRMResult | null }) {
if (!result) {
return null
}
return (
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 text-sm text-ink-50">
<div className="font-semibold text-ink-600">
输出目录:{result.output_dir}
</div>
<div className="mt-1">
新增 {result.generated} · 更新 {result.updated} · 跳过 {result.skipped} · 清理 {result.cleaned || 0}
</div>
{result.errors && result.errors.length > 0 && (
<div className="mt-2 text-red-500">
失败 {result.errors.length} 条:{result.errors.slice(0, 3).join(';')}
</div>
)}
</div>
)
}