perf(strm): 优化 OpenList 元数据下载速度 (#15)

* Add favourite buttons to library list and series detail pages

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites list for remote Emby mounted media

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Route favourites to library series detail when appropriate

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix favourites link to library series for remote anime paths

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix remote Emby playback history and continue watching

- Preserve duration_ms when web player reports duration=0
- Add GET /playback/:id/resume for reliable per-item resume
- Switch home continue watching to /watch-history/continue API
- Set library_id on remote media for play-profile visibility
- Keep continue-watching rows when remote metadata hydration fails

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix mobile sort dropdown overflowing off-screen

Align dropdown to button left edge on small screens and cap width
to viewport so sort options stay fully visible on mobile.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Remove stale Windows binary backup files

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* Fix STRM account edit form not echoing saved config

Return config_preview from account API for server/url/username and
secret flags. Merge partial config updates so empty password/token
fields do not wipe stored credentials.

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

* perf(strm): speed up OpenList metadata downloads with provider-aware concurrency

- Split download semaphore: 115 stays capped at 3 for WAF safety; OpenList/CloudDrive2
  WebDAV metadata uses strm.download_threads (default raised to 6, max 16)
- Scope 115 WAF cooldown to 115 tasks only so OpenList downloads are not paused
- Requeue claimed tasks back to pending when slot acquisition fails or 115 is cooling
- Add tests for separate semaphores and requeue behavior

Co-authored-by: truewhile <truewhile@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
truewhile
2026-09-02 15:15:49 +08:00
committed by GitHub
parent fbc33862fb
commit 67b85840dc
23 changed files with 515 additions and 91 deletions
+72 -17
View File
@@ -95,6 +95,26 @@ function Field({ label, hint, children }: { label: string; hint?: string; childr
const inputCls = 'input-base w-full'
const SECRET_PLACEHOLDER = '已配置,留空则不修改'
function accountDialogDefaults(existing: StrmAccount | null) {
const preview = existing?.config_preview
return {
provider: (existing?.provider ?? 'cloud115') as StrmProvider,
name: existing?.name ?? '',
enabled: existing?.enabled ?? true,
url: preview?.url ?? '',
server: preview?.server ?? '',
username: preview?.username ?? '',
password: '',
token: '',
proxyPlay: existing?.proxy_play ?? false,
embyLines: defaultEmbyRemoteLines(existing?.provider === 'emby_remote' ? existing?.emby_lines : undefined),
hasPassword: Boolean(preview?.has_password),
hasToken: Boolean(preview?.has_token || preview?.has_api_key),
}
}
// ─── 添加/编辑网盘账号 ────────────────────────────────────────────────────────
const PROVIDER_OPTIONS: { provider: StrmProvider; label: string; desc: string }[] = [
@@ -112,18 +132,19 @@ export function StrmAccountDialog({
onClose: () => void
onSaved: () => void
}) {
const [provider, setProvider] = useState<StrmProvider>(existing?.provider ?? 'cloud115')
const [name, setName] = useState(existing?.name ?? '')
const [enabled, setEnabled] = useState(existing?.enabled ?? true)
const [url, setUrl] = useState('')
const [embyLines, setEmbyLines] = useState<EmbyRemoteLine[]>(() =>
defaultEmbyRemoteLines(existing?.provider === 'emby_remote' ? existing?.emby_lines : undefined),
)
const [server, setServer] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [token, setToken] = useState('')
const [proxyPlay, setProxyPlay] = useState(existing?.proxy_play ?? false)
const defaults = accountDialogDefaults(existing)
const [provider, setProvider] = useState<StrmProvider>(defaults.provider)
const [name, setName] = useState(defaults.name)
const [enabled, setEnabled] = useState(defaults.enabled)
const [url, setUrl] = useState(defaults.url)
const [embyLines, setEmbyLines] = useState<EmbyRemoteLine[]>(defaults.embyLines)
const [server, setServer] = useState(defaults.server)
const [username, setUsername] = useState(defaults.username)
const [password, setPassword] = useState(defaults.password)
const [token, setToken] = useState(defaults.token)
const [proxyPlay, setProxyPlay] = useState(defaults.proxyPlay)
const [hasPassword, setHasPassword] = useState(defaults.hasPassword)
const [hasToken, setHasToken] = useState(defaults.hasToken)
const [saving, setSaving] = useState(false)
const buildConfig = (): Record<string, string> => {
@@ -178,7 +199,7 @@ export function StrmAccountDialog({
name,
provider: existing.provider,
enabled,
config: existing.has_credential && !Object.keys(buildConfig()).length ? {} : buildConfig(),
config: buildConfig(),
})
toast.success('网盘账号已更新')
} else {
@@ -364,7 +385,16 @@ export function StrmAccountDialog({
<input className={inputCls} value={username} onChange={(e) => setUsername(e.target.value)} />
</Field>
<Field label="密码">
<input className={inputCls} type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<input
className={inputCls}
type="password"
value={password}
placeholder={existing && hasPassword ? SECRET_PLACEHOLDER : undefined}
onChange={(e) => {
setPassword(e.target.value)
if (e.target.value) setHasPassword(false)
}}
/>
</Field>
</div>
)}
@@ -375,20 +405,45 @@ export function StrmAccountDialog({
<input className={inputCls} value={username} onChange={(e) => setUsername(e.target.value)} />
</Field>
<Field label="密码">
<input className={inputCls} type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<input
className={inputCls}
type="password"
value={password}
placeholder={existing && hasPassword ? SECRET_PLACEHOLDER : undefined}
onChange={(e) => {
setPassword(e.target.value)
if (e.target.value) setHasPassword(false)
}}
/>
</Field>
</div>
)}
{provider === 'emby_remote' && (
<Field label="API Key(可选)" hint="留空则用下方用户名/密码自动认证获取">
<input className={inputCls} value={token} placeholder="留空自动认证" onChange={(e) => setToken(e.target.value)} />
<input
className={inputCls}
value={token}
placeholder={existing && hasToken ? SECRET_PLACEHOLDER : '留空自动认证'}
onChange={(e) => {
setToken(e.target.value)
if (e.target.value) setHasToken(false)
}}
/>
</Field>
)}
{provider === 'openlist' && (
<Field label="Token(可选,优先于密码)">
<input className={inputCls} value={token} onChange={(e) => setToken(e.target.value)} />
<input
className={inputCls}
value={token}
placeholder={existing && hasToken ? SECRET_PLACEHOLDER : undefined}
onChange={(e) => {
setToken(e.target.value)
if (e.target.value) setHasToken(false)
}}
/>
</Field>
)}