mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
refactor strm generate section
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user