Files
MoonTVPlus/EMBY_INTEGRATION_DESIGN.md
2026-01-03 01:04:38 +08:00

617 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Emby 集成设计方案
## 一、可行性分析
### 1.1 Emby vs OpenList 对比
#### 相似点
| 特性 | OpenList | Emby |
|------|----------|------|
| 服务类型 | 外部 API 服务 | 外部 API 服务 |
| 认证方式 | 用户名/密码 + Token | API Key 或用户名/密码 |
| 媒体列表 | ✅ 提供 | ✅ 提供 |
| 播放链接 | ✅ 提供 | ✅ 提供 |
| 元数据 | 需自行获取 | ✅ 内置完整元数据 |
#### 关键差异
| 维度 | OpenList | Emby |
|------|----------|------|
| **元数据来源** | 项目通过 TMDB API 获取 | Emby 自带(已从 TMDB/TVDB 获取) |
| **API 结构** | 文件系统 API | 媒体库 API |
| **组织方式** | 文件夹结构 | 媒体库Movies/TV Shows |
| **播放链接** | 云存储直链 | 流媒体 URL支持转码 |
| **复杂度** | 需要文件名解析 + TMDB 查询 | 直接使用 Emby 元数据 |
### 1.2 可行性结论
**✅ 完全可行!** Emby 的集成甚至比 OpenList 更简单,因为:
- Emby 自带完整元数据系统
- API 更加成熟和标准化
- 不需要复杂的文件名解析和 TMDB 查询
- 媒体已经按照标准格式组织
---
## 二、CMS-Proxy 实现方案
### 2.1 为什么使用 CMS-Proxy
项目本身是聚合 CMS API 的(苹果 CMS V10 格式),使用 CMS-Proxy 的优势:
1. **网页播放**:复用现有播放逻辑,无需单独开发
2. **TVBOX 支持**:直接支持,无需额外适配
3. **代码复用**:统一接口,降低维护成本
4. **扩展性强**:未来添加其他源(如 Jellyfin只需实现 cms-proxy
### 2.2 CMS API 格式要求
```json
{
"code": 1,
"msg": "数据列表",
"page": 1,
"pagecount": 1,
"limit": 20,
"total": 100,
"list": [
{
"vod_id": "唯一ID",
"vod_name": "视频名称",
"vod_pic": "海报URL",
"vod_remarks": "备注(如'电影'/'剧集'",
"vod_year": "年份",
"vod_content": "简介",
"vod_play_from": "播放源名称",
"vod_play_url": "第1集$url1#第2集$url2#第3集$url3",
"type_name": "类型"
}
]
}
```
### 2.3 Emby 数据映射
```typescript
// Emby Item → CMS Format
{
vod_id: item.Id,
vod_name: item.Name,
vod_pic: `${embyUrl}/Items/${item.Id}/Images/Primary`,
vod_remarks: item.Type === 'Movie' ? '电影' : '剧集',
vod_year: item.ProductionYear?.toString() || '',
vod_content: item.Overview || '',
vod_play_from: 'Emby',
vod_play_url: buildPlayUrl(item),
type_name: item.Type === 'Movie' ? '电影' : '电视剧'
}
```
### 2.4 核心实现
#### 路由结构
```
/api/emby/cms-proxy/[token]?ac=videolist&wd=关键词
/api/emby/cms-proxy/[token]?ac=detail&ids=视频ID
```
#### 关键逻辑
**1. 搜索/列表**
```typescript
async function handleSearch(client: EmbyClient, query: string) {
const items = await client.getItems({
searchTerm: query,
IncludeItemTypes: 'Movie,Series',
Recursive: true,
Fields: 'Overview,ProductionYear',
Limit: 100
});
return items.map(item => ({
vod_id: item.Id,
vod_name: item.Name,
vod_pic: getEmbyImageUrl(item.Id),
vod_remarks: item.Type === 'Movie' ? '电影' : '剧集',
vod_year: item.ProductionYear?.toString() || '',
vod_content: item.Overview || '',
type_name: item.Type === 'Movie' ? '电影' : '电视剧'
}));
}
```
**2. 详情(关键)**
```typescript
async function handleDetail(client: EmbyClient, itemId: string) {
const item = await client.getItem(itemId);
let vodPlayUrl = '';
if (item.Type === 'Movie') {
// 电影:单个播放链接
const playUrl = buildPlayUrl(client, itemId);
vodPlayUrl = `正片$${playUrl}`;
} else if (item.Type === 'Series') {
// 剧集:获取所有季和集
const episodes = await getAllEpisodes(client, itemId);
vodPlayUrl = episodes
.map(ep => `${ep.IndexNumber}$${buildPlayUrl(client, ep.Id)}`)
.join('#');
}
return {
vod_id: item.Id,
vod_name: item.Name,
vod_play_url: vodPlayUrl,
vod_play_from: 'Emby',
// ... 其他字段
};
}
```
**3. 播放链接构建**
```typescript
function buildPlayUrl(client: EmbyClient, itemId: string): string {
// 方案 1: 直接返回 Emby 流媒体链接(推荐)
return `${client.serverUrl}/Videos/${itemId}/stream?api_key=${client.apiKey}&Static=true`;
// 方案 2: 通过项目代理(支持去广告等功能)
const baseUrl = process.env.SITE_BASE;
const token = process.env.TVBOX_SUBSCRIBE_TOKEN;
return `${baseUrl}/api/emby/play/${token}?id=${itemId}`;
}
```
---
## 三、前台页面集成方案
### 3.1 设计思路
将 Emby 集成到现有的"私人影库"页面,使用 **CapsuleSwitch** 组件切换 OpenList 和 Emby。
**优势:**
- 统一入口,用户体验更好
- 复用现有 UI 组件VideoCard、分页等
- 代码更简洁,维护成本低
### 3.2 页面结构
```
┌─────────────────────────────────────────┐
│ 私人影库 │
│ 观看自我收藏的高清视频吧 │
├─────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ OpenList │ Emby │ ← CapsuleSwitch
│ └─────────────────────────┘ │
│ │
├─────────────────────────────────────────┤
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ │
├─────────────────────────────────────────┤
│ 上一页 第 1 / 5 页 下一页 │
└─────────────────────────────────────────┘
```
### 3.3 核心代码
```typescript
// src/app/private-library/page.tsx
'use client';
import { useState, useEffect } from 'react';
import CapsuleSwitch from '@/components/CapsuleSwitch';
import VideoCard from '@/components/VideoCard';
type LibrarySource = 'openlist' | 'emby';
export default function PrivateLibraryPage() {
const [source, setSource] = useState<LibrarySource>('openlist');
const [videos, setVideos] = useState([]);
const [page, setPage] = useState(1);
// 切换源时重置页码
useEffect(() => {
setPage(1);
}, [source]);
useEffect(() => {
const endpoint = source === 'openlist'
? `/api/openlist/list?page=${page}`
: `/api/emby/list?page=${page}`;
fetch(endpoint).then(res => res.json()).then(setVideos);
}, [page, source]);
return (
<div>
<h1></h1>
{/* 源切换器 */}
<CapsuleSwitch
options={[
{ label: 'OpenList', value: 'openlist' },
{ label: 'Emby', value: 'emby' }
]}
active={source}
onChange={(value) => setSource(value as LibrarySource)}
/>
{/* 视频网格 */}
<div className="grid">
{videos.map(video => (
<VideoCard key={video.id} {...video} source={source} />
))}
</div>
{/* 分页 */}
</div>
);
}
```
---
## 四、完整实现步骤
### Phase 1: 基础设施
#### 1. 配置类型定义
```typescript
// src/lib/admin.types.ts
interface EmbyConfig {
Enabled: boolean
ServerURL: string // Emby 服务器地址
ApiKey?: string // API Key推荐
Username?: string // 或使用用户名/密码
Password?: string
UserId?: string // 用户 ID
Libraries?: string[] // 要显示的媒体库 ID
EnableTranscoding?: boolean // 是否启用转码
MaxBitrate?: number // 最大码率
LastSyncTime?: number
ItemCount?: number
}
```
#### 2. Emby 客户端
```typescript
// src/lib/emby.client.ts
export class EmbyClient {
private serverUrl: string
private apiKey?: string
private userId?: string
constructor(config: EmbyConfig) {
this.serverUrl = config.ServerURL.replace(/\/$/, '')
this.apiKey = config.ApiKey
this.userId = config.UserId
}
// 核心方法
async authenticate(username: string, password: string): Promise<AuthResult>
async getLibraries(): Promise<Library[]>
async getItems(params: GetItemsParams): Promise<ItemsResult>
async getItem(itemId: string): Promise<EmbyItem>
async getSeasons(seriesId: string): Promise<Season[]>
async getEpisodes(seriesId: string, seasonId: string): Promise<Episode[]>
async checkConnectivity(): Promise<boolean>
}
```
### Phase 2: API 端点
#### 1. CMS-Proxy核心
```
src/app/api/emby/cms-proxy/[token]/route.ts
```
- 支持搜索:`?ac=videolist&wd=关键词`
- 支持详情:`?ac=detail&ids=视频ID`
- 支持列表:`?ac=videolist`
- 返回苹果 CMS V10 格式
#### 2. 列表 API
```
src/app/api/emby/list/route.ts
```
- 用于前台页面显示
- 支持分页
- 返回格式与 OpenList 一致
#### 3. 详情 API
```
src/app/api/emby/detail/route.ts
```
- 获取媒体详情
- 获取剧集列表
- 返回播放链接
#### 4. 播放代理(可选)
```
src/app/api/emby/play/[token]/route.ts
```
- 代理播放请求
- 支持去广告等功能
#### 5. 管理 API
```
src/app/api/admin/emby/route.ts
```
- 保存/更新 Emby 配置
- 测试连接
- 同步媒体库
### Phase 3: 前台集成
#### 1. 修改私人影库页面
```
src/app/private-library/page.tsx
```
- 添加 CapsuleSwitch 组件
- 支持切换 OpenList/Emby
- 复用现有 VideoCard 和分页逻辑
#### 2. 修改播放页面
```
src/app/play/page.tsx
```
- 支持 `source=emby` 参数
- 处理 Emby 播放链接
#### 3. 管理面板集成
```
src/app/admin/page.tsx
```
- 添加 Emby 配置区域
- 连接测试
- 媒体库同步
#### 4. 导航集成
```
src/components/Sidebar.tsx
```
- 保持"私人影库"入口不变
- 内部通过 CapsuleSwitch 切换
### Phase 4: TVBOX 集成
#### 修改订阅接口
```typescript
// src/app/api/tvbox/subscribe/[token]/route.ts
const sites = [
// 现有的 CMS 站点
...existingSites,
// 添加 OpenList
{
key: 'openlist',
name: '私人影库-OpenList',
type: 1,
api: `${baseUrl}/api/cms-proxy?api=openlist`,
searchable: 1
},
// 添加 Emby
{
key: 'emby',
name: '私人影库-Emby',
type: 1,
api: `${baseUrl}/api/emby/cms-proxy/${token}`,
searchable: 1
}
];
```
---
## 五、文件结构
```
src/
├── app/
│ ├── api/
│ │ ├── admin/
│ │ │ └── emby/
│ │ │ └── route.ts # Emby 配置管理
│ │ └── emby/
│ │ ├── cms-proxy/
│ │ │ └── [token]/
│ │ │ └── route.ts # CMS API 代理(核心)
│ │ ├── list/
│ │ │ └── route.ts # 媒体列表
│ │ ├── detail/
│ │ │ └── route.ts # 媒体详情
│ │ └── play/
│ │ └── [token]/
│ │ └── route.ts # 播放代理(可选)
│ ├── private-library/
│ │ └── page.tsx # 私人影库页面(集成 CapsuleSwitch
│ └── admin/
│ └── page.tsx # 管理面板(添加 Emby 配置)
├── lib/
│ ├── emby.client.ts # Emby API 客户端
│ ├── emby-adapter.ts # 数据适配器
│ ├── emby-cache.ts # 缓存层(可选)
│ └── admin.types.ts # 类型定义(添加 EmbyConfig
└── components/
├── CapsuleSwitch.tsx # 切换组件(已存在)
└── VideoCard.tsx # 视频卡片(已存在)
```
---
## 六、关键技术点
### 6.1 Emby API 认证
```typescript
// 方式 1: API Key推荐
const headers = {
'X-Emby-Token': apiKey
}
// 方式 2: 用户认证
const authResponse = await fetch(`${serverUrl}/Users/AuthenticateByName`, {
method: 'POST',
body: JSON.stringify({ Username: username, Pw: password })
});
const { AccessToken, User } = await authResponse.json();
```
### 6.2 获取媒体列表
```typescript
// GET /Users/{userId}/Items
const params = new URLSearchParams({
ParentId: libraryId,
IncludeItemTypes: 'Movie,Series',
Recursive: 'true',
Fields: 'Overview,ProductionYear',
StartIndex: '0',
Limit: '20'
});
```
### 6.3 获取播放链接
```typescript
// 直接播放(无转码)
const playUrl = `${serverUrl}/Videos/${itemId}/stream?api_key=${apiKey}&Static=true`;
// 转码播放
const playUrl = `${serverUrl}/Videos/${itemId}/master.m3u8?api_key=${apiKey}&VideoCodec=h264`;
```
### 6.4 获取剧集
```typescript
// 获取季列表
const seasons = await fetch(`${serverUrl}/Shows/${seriesId}/Seasons?userId=${userId}`);
// 获取某季的所有集
const episodes = await fetch(`${serverUrl}/Shows/${seriesId}/Episodes?seasonId=${seasonId}`);
```
---
## 七、优势总结
### 7.1 相比传统方式
| 特性 | 传统方式 | CMS-Proxy 方式 |
|------|---------|---------------|
| **网页播放** | 需要单独开发 Emby 播放页面 | 复用现有播放逻辑 |
| **TVBOX 支持** | 需要单独适配 | 直接支持,无需额外开发 |
| **代码复用** | 低 | 高 |
| **维护成本** | 高(两套逻辑) | 低(统一接口) |
| **扩展性** | 每个源都要适配 | 新增源只需实现 cms-proxy |
### 7.2 相比 OpenList
| 维度 | OpenList | Emby |
|------|----------|------|
| **实现复杂度** | 复杂(需要文件名解析 + TMDB 查询) | 简单(直接使用 Emby 元数据) |
| **元数据质量** | 依赖 TMDB API | Emby 自带完整元数据 |
| **播放能力** | 云存储直链 | 支持转码、字幕、多音轨 |
| **媒体组织** | 基于文件夹 | 标准媒体库结构 |
---
## 八、预估工作量
- **核心功能**:约 800-1000 行代码
- **完整集成**:约 1200-1500 行代码
- **开发时间**2-3 天(有 OpenList 参考)
### 代码量分解
| 模块 | 代码量 | 说明 |
|------|--------|------|
| Emby 客户端 | 200-300 行 | API 封装 |
| CMS-Proxy | 300-400 行 | 核心转换逻辑 |
| 列表/详情 API | 200-300 行 | 前台接口 |
| 前台页面修改 | 100-150 行 | CapsuleSwitch 集成 |
| 管理面板 | 150-200 行 | 配置界面 |
| 类型定义 | 50-100 行 | TypeScript 类型 |
| **总计** | **1000-1450 行** | |
---
## 九、实现建议
1. **优先使用 API Key 认证**(比用户名/密码更简单)
2. **实现良好的缓存策略**(减少对 Emby 服务器的请求)
3. **支持多媒体库**(让用户选择显示哪些库)
4. **考虑转码选项**(根据网络情况动态调整)
5. **复用现有组件**VideoCard、播放器等
6. **先实现 CMS-Proxy**(这样 TVBOX 和网页都能用)
7. **再实现前台集成**CapsuleSwitch 切换)
---
## 十、测试计划
### 10.1 功能测试
- [ ] Emby 连接测试
- [ ] 媒体列表获取(电影/剧集)
- [ ] 搜索功能
- [ ] 详情页面(电影/剧集)
- [ ] 播放功能(直接播放/转码)
- [ ] 剧集切换
- [ ] 分页功能
- [ ] CapsuleSwitch 切换
### 10.2 集成测试
- [ ] 网页播放测试
- [ ] TVBOX 订阅测试
- [ ] TVBOX 搜索测试
- [ ] TVBOX 播放测试
- [ ] OpenList 和 Emby 切换测试
### 10.3 性能测试
- [ ] 大型媒体库加载速度
- [ ] 缓存效果验证
- [ ] 并发请求处理
---
## 十一、后续扩展
### 可能的增强功能
1. **媒体库过滤**:按类型、年份、评分过滤
2. **收藏功能**:标记喜欢的媒体
3. **观看历史**:记录播放进度
4. **字幕支持**:加载 Emby 字幕
5. **转码设置**:用户自定义转码参数
6. **多用户支持**:不同用户看到不同的媒体库
7. **Jellyfin 支持**Jellyfin API 与 Emby 类似,可以复用大部分代码
---
## 十二、总结
Emby 集成方案通过 **CMS-Proxy****CapsuleSwitch** 的组合,实现了:
**统一接口**:网页和 TVBOX 都使用 CMS API 格式
**统一入口**:私人影库页面集成 OpenList 和 Emby
**代码复用**:最大化复用现有组件和逻辑
**扩展性强**未来添加其他源Jellyfin、Plex只需实现 cms-proxy
**用户体验好**:无缝切换,流畅播放
这是一个**优雅、高效、可扩展**的设计方案!