feat: Emby 远程挂载(联邦聚合)功能 beta

- 新增 emby_remote 提供方:通过 STRM 账号体系配置远程 Emby 地址/用户名/密码
- EmbyService 聚合远程媒体库 Views/Items/详情/最近添加/全局搜索,远程数据不落库
- 条目 ID 统一伪装为 embyremote~{account}~{id},请求按账号路由回远程
- 播放支持账号级 proxy_play 配置:默认客户端直连远程;开启后由 MMTL 反向代理流/字幕
- 图片/元数据全部实时透传远程,播放状态(已看/收藏)透传回远程 Emby
- cloud 包新增 emby Provider(Ping/List/Resolve),账号测试/目录浏览自动生效
- 前端 STRM 页面支持 Emby 远程挂载账号(地址+凭据+代理开关)
This commit is contained in:
truewhile
2026-09-01 10:44:01 +08:00
parent 89d7a6cbb2
commit 0eb3f104f4
21 changed files with 1748 additions and 15 deletions
+62 -2
View File
@@ -10,6 +10,7 @@ import {
QrCode,
RefreshCw,
Search,
Tv,
X,
} from 'lucide-react'
@@ -90,6 +91,7 @@ const PROVIDER_OPTIONS: { provider: StrmProvider; label: string; desc: string }[
{ provider: 'cloud115', label: '115 网盘', desc: '开放平台 OAuth 授权(扫码/网页)' },
{ provider: 'clouddrive2', label: 'CloudDrive2', desc: 'WebDAV 桥接多种网盘' },
{ provider: 'openlist', label: 'OpenList', desc: 'OpenList / AList 兼容服务' },
{ provider: 'emby_remote', label: 'Emby 远程挂载', desc: '聚合远程 Emby 媒体库,不落库实时透传' },
]
export function StrmAccountDialog({
@@ -109,6 +111,7 @@ export function StrmAccountDialog({
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [token, setToken] = useState('')
const [proxyPlay, setProxyPlay] = useState(false)
const [saving, setSaving] = useState(false)
const buildConfig = (): Record<string, string> => {
@@ -126,6 +129,14 @@ export function StrmAccountDialog({
...(password ? { password } : {}),
...(token ? { token } : {}),
}
case 'emby_remote':
return {
...(url ? { url } : {}),
...(username ? { username } : {}),
...(password ? { password } : {}),
...(token ? { token } : {}),
proxy_play: proxyPlay ? 'true' : 'false',
}
default:
return {}
}
@@ -137,6 +148,8 @@ export function StrmAccountDialog({
return url !== ''
case 'openlist':
return server !== '' && (token !== '' || password !== '')
case 'emby_remote':
return url !== '' && (token !== '' || password !== '')
default:
return false
}
@@ -182,7 +195,14 @@ export function StrmAccountDialog({
{!existing && (
<div className="grid grid-cols-3 gap-3">
{PROVIDER_OPTIONS.map((option) => {
const Icon = option.provider === 'cloud115' ? QrCode : option.provider === 'clouddrive2' ? HardDrive : FolderPlus
const Icon =
option.provider === 'cloud115'
? QrCode
: option.provider === 'clouddrive2'
? HardDrive
: option.provider === 'emby_remote'
? Tv
: FolderPlus
const active = provider === option.provider
return (
<button
@@ -235,7 +255,13 @@ export function StrmAccountDialog({
</Field>
)}
{provider !== 'cloud115' && (
{provider === 'emby_remote' && (
<Field label="Emby 服务地址" hint="远程 Emby 服务器地址,如 http://192.168.1.10:8096">
<input className={inputCls} value={url} placeholder="http://host:8096" onChange={(e) => setUrl(e.target.value)} />
</Field>
)}
{(provider === 'clouddrive2' || provider === 'emby_remote') && (
<div className="grid grid-cols-2 gap-3">
<Field label="用户名">
<input className={inputCls} value={username} onChange={(e) => setUsername(e.target.value)} />
@@ -246,12 +272,46 @@ export function StrmAccountDialog({
</div>
)}
{provider === 'openlist' && (
<div className="grid grid-cols-2 gap-3">
<Field label="用户名">
<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)} />
</Field>
</div>
)}
{provider === 'emby_remote' && (
<Field label="API Key(可选)" hint="留空则用下方用户名/密码自动认证获取">
<input className={inputCls} value={token} placeholder="留空自动认证" onChange={(e) => setToken(e.target.value)} />
</Field>
)}
{provider === 'openlist' && (
<Field label="Token(可选,优先于密码)">
<input className={inputCls} value={token} onChange={(e) => setToken(e.target.value)} />
</Field>
)}
{provider === 'emby_remote' && (
<label className="flex cursor-pointer items-start gap-2 text-sm text-ink-100">
<input
type="checkbox"
className="mt-0.5 h-4 w-4 accent-primary-400"
checked={proxyPlay}
onChange={(e) => setProxyPlay(e.target.checked)}
/>
<span>
<span className="font-semibold text-ink-600">播放流量经过本服务器代理</span>
<span className="block text-xs text-sand-500">
关闭(推荐):播放时客户端直连远程 Emby,本服务器不参与流量中转
</span>
</span>
</label>
)}
{existing && provider !== 'cloud115' && (
<label className="flex cursor-pointer items-center gap-2 text-sm text-ink-100">
<input
+3
View File
@@ -15,6 +15,7 @@ import {
RefreshCw,
Settings,
Trash2,
Tv,
Upload,
} from 'lucide-react'
@@ -55,6 +56,8 @@ export function providerIcon(provider: StrmProvider) {
return FolderPlus
case 'local':
return HardDrive
case 'emby_remote':
return Tv
}
}
+2 -1
View File
@@ -1,11 +1,12 @@
// STRM 管理相关类型定义(与后端 internal/model/strm.go 对应)。
export type StrmProvider = 'cloud115' | 'clouddrive2' | 'openlist' | 'local'
export type StrmProvider = 'cloud115' | 'clouddrive2' | 'openlist' | 'local' | 'emby_remote'
export const STRM_PROVIDER_LABELS: Record<StrmProvider, string> = {
cloud115: '115 网盘',
clouddrive2: 'CloudDrive2',
openlist: 'OpenList',
local: '本地目录',
emby_remote: 'Emby 远程挂载',
}
export interface StrmAccount {