修改继续观看emby样式
This commit is contained in:
@@ -1,616 +0,0 @@
|
||||
# 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
|
||||
✅ **用户体验好**:无缝切换,流畅播放
|
||||
|
||||
这是一个**优雅、高效、可扩展**的设计方案!
|
||||
@@ -1,287 +0,0 @@
|
||||
# Emby 集成使用指南
|
||||
|
||||
## 🎉 实现完成
|
||||
|
||||
Emby 已经成功集成到 MoonTVPlus 项目中!所有核心功能都已实现并可以使用。
|
||||
|
||||
---
|
||||
|
||||
## 📋 已实现的功能
|
||||
|
||||
### 1. 核心基础设施
|
||||
- ✅ EmbyConfig 类型定义 (`src/lib/admin.types.ts`)
|
||||
- ✅ Emby 客户端 (`src/lib/emby.client.ts`)
|
||||
|
||||
### 2. API 端点
|
||||
- ✅ `/api/emby/list` - 媒体列表(分页)
|
||||
- ✅ `/api/emby/detail` - 媒体详情
|
||||
- ✅ `/api/emby/cms-proxy/[token]` - CMS API 代理(核心)
|
||||
- ✅ `/api/admin/emby` - 管理配置
|
||||
|
||||
### 3. 前台集成
|
||||
- ✅ 私人影库页面集成 CapsuleSwitch(OpenList ↔ Emby 切换)
|
||||
- ✅ 播放页面支持 `source=emby` 参数
|
||||
|
||||
### 4. 管理面板
|
||||
- ✅ Emby 配置区域(服务器地址、API Key、用户名/密码)
|
||||
- ✅ 连接测试功能
|
||||
- ✅ 保存配置功能
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 步骤 1: 配置 Emby
|
||||
|
||||
1. 登录管理面板
|
||||
2. 找到 **"Emby 媒体库"** 配置区域
|
||||
3. 填写以下信息:
|
||||
- **服务器地址**: 你的 Emby 服务器地址(如 `http://192.168.1.100:8096`)
|
||||
- **API Key**(推荐): 在 Emby 设置中生成 API Key
|
||||
- 或者填写 **用户名** 和 **密码**
|
||||
4. 点击 **"测试连接"** 验证配置
|
||||
5. 点击 **"保存配置"**
|
||||
6. 启用 **"启用 Emby 媒体库"** 开关
|
||||
|
||||
### 步骤 2: 访问私人影库
|
||||
|
||||
1. 访问 **"私人影库"** 页面
|
||||
2. 使用顶部的 **CapsuleSwitch** 切换到 **"Emby"**
|
||||
3. 浏览你的 Emby 媒体库
|
||||
4. 点击任意视频即可播放
|
||||
|
||||
### 步骤 3: TVBOX 订阅(可选)
|
||||
|
||||
如果你启用了 TVBOX 订阅功能,Emby 会自动添加到订阅源中:
|
||||
|
||||
**订阅地址格式**:
|
||||
```
|
||||
http://your-domain.com/api/emby/cms-proxy/{TVBOX_SUBSCRIBE_TOKEN}
|
||||
```
|
||||
|
||||
在 TVBOX 中添加此订阅源,即可在 TVBOX 中浏览和播放 Emby 媒体库。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### Emby 服务器地址
|
||||
|
||||
- 格式: `http://IP:PORT` 或 `https://domain.com`
|
||||
- 示例: `http://192.168.1.100:8096`
|
||||
- 注意: 确保 MoonTVPlus 服务器能够访问 Emby 服务器
|
||||
|
||||
### 认证方式
|
||||
|
||||
**方式 1: API Key(推荐)**
|
||||
- 在 Emby 设置中生成 API Key
|
||||
- 优点: 更安全,不需要存储密码
|
||||
- 获取方式: Emby 设置 → 高级 → API Keys
|
||||
|
||||
**方式 2: 用户名/密码**
|
||||
- 使用 Emby 用户账号
|
||||
- 优点: 简单直接
|
||||
- 注意: 密码会加密存储在数据库中
|
||||
|
||||
---
|
||||
|
||||
## 📖 功能特性
|
||||
|
||||
### 1. 网页播放
|
||||
|
||||
- **统一入口**: 私人影库页面
|
||||
- **无缝切换**: CapsuleSwitch 在 OpenList 和 Emby 之间切换
|
||||
- **完整支持**: 电影、剧集、季、集
|
||||
- **自动播放**: 点击即播,无需额外配置
|
||||
|
||||
### 2. CMS-Proxy
|
||||
|
||||
Emby 的 CMS-Proxy 将 Emby 媒体库转换为苹果 CMS V10 格式,实现:
|
||||
|
||||
- ✅ 网页播放复用现有逻辑
|
||||
- ✅ TVBOX 直接支持
|
||||
- ✅ 统一接口,降低维护成本
|
||||
- ✅ 扩展性强,未来可添加 Jellyfin、Plex 等
|
||||
|
||||
### 3. 播放链接
|
||||
|
||||
**直接播放**(默认):
|
||||
```
|
||||
http://emby-server:8096/Videos/{itemId}/stream?Static=true&api_key={apiKey}
|
||||
```
|
||||
|
||||
**转码播放**(可选):
|
||||
```
|
||||
http://emby-server:8096/Videos/{itemId}/master.m3u8?api_key={apiKey}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 场景 1: 家庭媒体中心
|
||||
|
||||
- 在家中搭建 Emby 服务器
|
||||
- 使用 MoonTVPlus 作为统一的观影入口
|
||||
- 在私人影库中浏览和播放 Emby 媒体
|
||||
|
||||
### 场景 2: 多源聚合
|
||||
|
||||
- OpenList: 云存储媒体
|
||||
- Emby: 本地媒体服务器
|
||||
- CMS 源: 在线资源
|
||||
- 统一在 MoonTVPlus 中管理和播放
|
||||
|
||||
### 场景 3: TVBOX 集成
|
||||
|
||||
- 在 TVBOX 中添加 Emby 订阅源
|
||||
- 在电视上浏览和播放 Emby 媒体
|
||||
- 无需单独安装 Emby 客户端
|
||||
|
||||
---
|
||||
|
||||
## 🔍 故障排查
|
||||
|
||||
### 问题 1: 连接测试失败
|
||||
|
||||
**可能原因**:
|
||||
- Emby 服务器地址错误
|
||||
- 网络不通(防火墙、端口未开放)
|
||||
- API Key 或用户名/密码错误
|
||||
|
||||
**解决方法**:
|
||||
1. 检查 Emby 服务器地址是否正确
|
||||
2. 确保 MoonTVPlus 服务器能够访问 Emby 服务器
|
||||
3. 验证 API Key 或用户名/密码是否正确
|
||||
4. 检查 Emby 服务器日志
|
||||
|
||||
### 问题 2: 媒体列表为空
|
||||
|
||||
**可能原因**:
|
||||
- Emby 媒体库为空
|
||||
- 用户权限不足
|
||||
- 媒体库未扫描完成
|
||||
|
||||
**解决方法**:
|
||||
1. 在 Emby 中检查媒体库是否有内容
|
||||
2. 确保使用的用户有访问媒体库的权限
|
||||
3. 等待 Emby 完成媒体库扫描
|
||||
|
||||
### 问题 3: 播放失败
|
||||
|
||||
**可能原因**:
|
||||
- 媒体文件损坏
|
||||
- 网络带宽不足
|
||||
- 浏览器不支持视频格式
|
||||
|
||||
**解决方法**:
|
||||
1. 在 Emby 客户端中测试播放
|
||||
2. 检查网络连接
|
||||
3. 尝试使用其他浏览器
|
||||
4. 考虑启用转码
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 1. 缓存策略
|
||||
|
||||
- 媒体列表缓存: 减少对 Emby 服务器的请求
|
||||
- 播放链接缓存: 提高播放响应速度
|
||||
|
||||
### 2. 网络优化
|
||||
|
||||
- 使用本地网络: Emby 服务器和 MoonTVPlus 在同一网络
|
||||
- 启用转码: 根据网络情况动态调整码率
|
||||
|
||||
### 3. 媒体库优化
|
||||
|
||||
- 定期清理无效媒体
|
||||
- 使用 SSD 存储媒体文件
|
||||
- 优化 Emby 服务器配置
|
||||
|
||||
---
|
||||
|
||||
## 🔐 安全建议
|
||||
|
||||
1. **使用 HTTPS**: 在生产环境中使用 HTTPS 访问 Emby
|
||||
2. **强密码**: 使用强密码保护 Emby 账号
|
||||
3. **API Key**: 优先使用 API Key 而不是用户名/密码
|
||||
4. **防火墙**: 限制 Emby 服务器的访问来源
|
||||
5. **定期更新**: 保持 Emby 和 MoonTVPlus 更新到最新版本
|
||||
|
||||
---
|
||||
|
||||
## 🎨 自定义
|
||||
|
||||
### 修改播放链接格式
|
||||
|
||||
编辑 `src/lib/emby.client.ts` 中的 `getStreamUrl` 方法:
|
||||
|
||||
```typescript
|
||||
getStreamUrl(itemId: string, direct: boolean = true): string {
|
||||
if (direct) {
|
||||
// 直接播放
|
||||
return `${this.serverUrl}/Videos/${itemId}/stream?Static=true&api_key=${this.apiKey}`;
|
||||
}
|
||||
// 转码播放
|
||||
return `${this.serverUrl}/Videos/${itemId}/master.m3u8?api_key=${this.apiKey}&VideoCodec=h264`;
|
||||
}
|
||||
```
|
||||
|
||||
### 添加媒体库过滤
|
||||
|
||||
在 `src/lib/admin.types.ts` 中的 `EmbyConfig` 添加 `Libraries` 字段,然后在 API 中使用:
|
||||
|
||||
```typescript
|
||||
const result = await client.getItems({
|
||||
ParentId: config.EmbyConfig.Libraries?.[0], // 指定媒体库
|
||||
IncludeItemTypes: 'Movie,Series',
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚧 已知限制
|
||||
|
||||
1. **字幕支持**: 当前版本不支持加载 Emby 字幕(计划中)
|
||||
2. **转码设置**: 不支持用户自定义转码参数(计划中)
|
||||
3. **多用户**: 不支持不同用户看到不同的媒体库(计划中)
|
||||
4. **收藏同步**: 不支持与 Emby 的收藏同步(计划中)
|
||||
|
||||
---
|
||||
|
||||
## 🔮 未来计划
|
||||
|
||||
- [ ] 字幕支持
|
||||
- [ ] 转码设置
|
||||
- [ ] 多用户支持
|
||||
- [ ] 收藏同步
|
||||
- [ ] 观看历史同步
|
||||
- [ ] Jellyfin 支持(API 与 Emby 类似)
|
||||
- [ ] Plex 支持
|
||||
|
||||
---
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如果遇到问题或有建议,请:
|
||||
|
||||
1. 查看本文档的故障排查部分
|
||||
2. 查看 Emby 服务器日志
|
||||
3. 查看 MoonTVPlus 服务器日志
|
||||
4. 在 GitHub 上提交 Issue
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
Emby 集成为 MoonTVPlus 带来了强大的本地媒体服务器支持,通过 CMS-Proxy 的设计,实现了:
|
||||
|
||||
- ✅ 统一的播放体验
|
||||
- ✅ TVBOX 无缝集成
|
||||
- ✅ 代码复用和维护简化
|
||||
- ✅ 良好的扩展性
|
||||
|
||||
享受你的 Emby 媒体库吧!🎬
|
||||
@@ -1219,7 +1219,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
{config.showSourceName && source_name && !cmsData && (
|
||||
<span
|
||||
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/30 backdrop-blur-sm ${
|
||||
actualSource === 'openlist' ? 'border-yellow-500' : 'border-white/60'
|
||||
actualSource === 'openlist' || actualSource === 'emby' ? 'border-yellow-500' : 'border-white/60'
|
||||
}`}
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
@@ -1269,7 +1269,7 @@ const VideoCard = forwardRef<VideoCardHandle, VideoCardProps>(function VideoCard
|
||||
<div className='flex items-center justify-end'>
|
||||
<span
|
||||
className={`inline-block border rounded px-1 py-0.5 text-[8px] text-white/90 bg-black/30 backdrop-blur-sm ${
|
||||
origin === 'live' ? 'border-red-500' : actualSource === 'openlist' ? 'border-yellow-500' : 'border-white/60'
|
||||
origin === 'live' ? 'border-red-500' : actualSource === 'openlist' || actualSource === 'emby' ? 'border-yellow-500' : 'border-white/60'
|
||||
}`}
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
|
||||
Reference in New Issue
Block a user