emby高级参数
This commit is contained in:
@@ -3426,6 +3426,10 @@ const EmbyConfigComponent = ({
|
||||
Password: '',
|
||||
UserId: '',
|
||||
isDefault: false,
|
||||
// 高级选项
|
||||
removeEmbyPrefix: false,
|
||||
appendMediaSourceId: false,
|
||||
transcodeMp4: false,
|
||||
});
|
||||
|
||||
// 从配置加载源列表
|
||||
@@ -3460,6 +3464,10 @@ const EmbyConfigComponent = ({
|
||||
Password: '',
|
||||
UserId: '',
|
||||
isDefault: false,
|
||||
// 高级选项
|
||||
removeEmbyPrefix: false,
|
||||
appendMediaSourceId: false,
|
||||
transcodeMp4: false,
|
||||
});
|
||||
setEditingSource(null);
|
||||
setShowAddForm(false);
|
||||
@@ -3912,6 +3920,85 @@ const EmbyConfigComponent = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 高级选项 */}
|
||||
<div className='border-t border-gray-200 dark:border-gray-700 pt-4 mt-4'>
|
||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-3'>
|
||||
高级选项
|
||||
</h4>
|
||||
|
||||
{/* 选项1: 播放链接移除/emby前缀 */}
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='flex-1'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
播放链接移除/emby前缀
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
启用后将从播放链接中移除 /emby 前缀
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFormData({ ...formData, removeEmbyPrefix: !formData.removeEmbyPrefix })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.removeEmbyPrefix ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.removeEmbyPrefix ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 选项2: 拼接MediaSourceId参数 */}
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='flex-1'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
拼接MediaSourceId参数
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
启用后将调用 PlaybackInfo API 获取 MediaSourceId 并添加到播放链接
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFormData({ ...formData, appendMediaSourceId: !formData.appendMediaSourceId })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.appendMediaSourceId ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.appendMediaSourceId ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 选项3: 转码mp4 */}
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<div className='flex-1'>
|
||||
<label className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||
转码mp4
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
启用后将使用 stream.mp4 格式并移除 Static 参数
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setFormData({ ...formData, transcodeMp4: !formData.transcodeMp4 })}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
formData.transcodeMp4 ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
formData.transcodeMp4 ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className='flex gap-3 pt-4'>
|
||||
<button
|
||||
|
||||
@@ -28,21 +28,23 @@ export async function GET(request: NextRequest) {
|
||||
// 获取所有剧集
|
||||
const allEpisodes = await client.getEpisodes(itemId);
|
||||
|
||||
episodes = allEpisodes
|
||||
.sort((a, b) => {
|
||||
if (a.ParentIndexNumber !== b.ParentIndexNumber) {
|
||||
return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
|
||||
}
|
||||
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
|
||||
})
|
||||
.map((ep) => ({
|
||||
id: ep.Id,
|
||||
title: ep.Name,
|
||||
episode: ep.IndexNumber || 0,
|
||||
season: ep.ParentIndexNumber || 1,
|
||||
overview: ep.Overview || '',
|
||||
playUrl: client.getStreamUrl(ep.Id),
|
||||
}));
|
||||
episodes = await Promise.all(
|
||||
allEpisodes
|
||||
.sort((a, b) => {
|
||||
if (a.ParentIndexNumber !== b.ParentIndexNumber) {
|
||||
return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
|
||||
}
|
||||
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
|
||||
})
|
||||
.map(async (ep) => ({
|
||||
id: ep.Id,
|
||||
title: ep.Name,
|
||||
episode: ep.IndexNumber || 0,
|
||||
season: ep.ParentIndexNumber || 1,
|
||||
overview: ep.Overview || '',
|
||||
playUrl: await client.getStreamUrl(ep.Id),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -55,7 +57,7 @@ export async function GET(request: NextRequest) {
|
||||
poster: client.getImageUrl(item.Id, 'Primary'),
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
rating: item.CommunityRating || 0,
|
||||
playUrl: item.Type === 'Movie' ? client.getStreamUrl(item.Id) : undefined,
|
||||
playUrl: item.Type === 'Movie' ? await client.getStreamUrl(item.Id) : undefined,
|
||||
},
|
||||
episodes: item.Type === 'Series' ? episodes : [],
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function GET(
|
||||
let client = await getEmbyClient(embyKey);
|
||||
|
||||
// 构建 Emby 原始播放链接
|
||||
let embyStreamUrl = client.getStreamUrl(itemId);
|
||||
let embyStreamUrl = await client.getStreamUrl(itemId);
|
||||
|
||||
// 构建请求头,转发 Range 请求
|
||||
const requestHeaders: HeadersInit = {};
|
||||
@@ -81,7 +81,7 @@ export async function GET(
|
||||
const { embyManager } = await import('@/lib/emby-manager');
|
||||
embyManager.clearCache();
|
||||
client = await getEmbyClient(embyKey);
|
||||
embyStreamUrl = client.getStreamUrl(itemId);
|
||||
embyStreamUrl = await client.getStreamUrl(itemId);
|
||||
videoResponse = await fetch(embyStreamUrl, {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function GET(request: NextRequest) {
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
douban_id: 0,
|
||||
desc: item.Overview || '',
|
||||
episodes: [client.getStreamUrl(item.Id)],
|
||||
episodes: [await client.getStreamUrl(item.Id)],
|
||||
episodes_titles: [item.Name],
|
||||
subtitles: subtitles.length > 0 ? [subtitles] : [],
|
||||
proxyMode: false,
|
||||
@@ -102,7 +102,7 @@ export async function GET(request: NextRequest) {
|
||||
year: item.ProductionYear?.toString() || '',
|
||||
douban_id: 0,
|
||||
desc: item.Overview || '',
|
||||
episodes: allEpisodes.map((ep) => client.getStreamUrl(ep.Id)),
|
||||
episodes: await Promise.all(allEpisodes.map((ep) => client.getStreamUrl(ep.Id))),
|
||||
episodes_titles: allEpisodes.map((ep) => {
|
||||
const seasonNum = ep.ParentIndexNumber || 1;
|
||||
const episodeNum = ep.IndexNumber || 1;
|
||||
|
||||
@@ -171,6 +171,10 @@ export interface AdminConfig {
|
||||
LastSyncTime?: number; // 最后同步时间戳
|
||||
ItemCount?: number; // 媒体项数量
|
||||
isDefault?: boolean; // 是否为默认源(用于向后兼容)
|
||||
// 高级流媒体选项
|
||||
removeEmbyPrefix?: boolean; // 播放链接移除/emby前缀
|
||||
appendMediaSourceId?: boolean; // 拼接MediaSourceId参数
|
||||
transcodeMp4?: boolean; // 转码mp4
|
||||
}>;
|
||||
// 旧格式:单源配置(向后兼容)
|
||||
Enabled?: boolean;
|
||||
|
||||
@@ -18,6 +18,10 @@ interface EmbySourceConfig {
|
||||
LastSyncTime?: number;
|
||||
ItemCount?: number;
|
||||
isDefault?: boolean;
|
||||
// 高级流媒体选项
|
||||
removeEmbyPrefix?: boolean;
|
||||
appendMediaSourceId?: boolean;
|
||||
transcodeMp4?: boolean;
|
||||
}
|
||||
|
||||
class EmbyManager {
|
||||
@@ -174,6 +178,10 @@ export function migrateEmbyConfig(config: AdminConfig): AdminConfig {
|
||||
LastSyncTime: oldConfig.LastSyncTime,
|
||||
ItemCount: oldConfig.ItemCount,
|
||||
isDefault: true,
|
||||
// 高级选项默认值
|
||||
removeEmbyPrefix: false,
|
||||
appendMediaSourceId: false,
|
||||
transcodeMp4: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ interface EmbyConfig {
|
||||
Password?: string;
|
||||
UserId?: string;
|
||||
AuthToken?: string;
|
||||
// 高级流媒体选项
|
||||
removeEmbyPrefix?: boolean;
|
||||
appendMediaSourceId?: boolean;
|
||||
transcodeMp4?: boolean;
|
||||
}
|
||||
|
||||
interface EmbyItem {
|
||||
@@ -64,13 +68,28 @@ export class EmbyClient {
|
||||
private authToken?: string;
|
||||
private username?: string;
|
||||
private password?: string;
|
||||
private removeEmbyPrefix: boolean;
|
||||
private appendMediaSourceId: boolean;
|
||||
private transcodeMp4: boolean;
|
||||
|
||||
constructor(config: EmbyConfig) {
|
||||
let serverUrl = config.ServerURL.replace(/\/$/, '');
|
||||
// 如果 URL 不包含 /emby 路径,自动添加
|
||||
if (!serverUrl.endsWith('/emby')) {
|
||||
|
||||
// 存储高级选项
|
||||
this.removeEmbyPrefix = config.removeEmbyPrefix || false;
|
||||
this.appendMediaSourceId = config.appendMediaSourceId || false;
|
||||
this.transcodeMp4 = config.transcodeMp4 || false;
|
||||
|
||||
// 如果 URL 不包含 /emby 路径,自动添加(除非启用了 removeEmbyPrefix)
|
||||
if (!serverUrl.endsWith('/emby') && !this.removeEmbyPrefix) {
|
||||
serverUrl += '/emby';
|
||||
}
|
||||
|
||||
// 如果启用了 removeEmbyPrefix 且 URL 包含 /emby,移除它
|
||||
if (this.removeEmbyPrefix && serverUrl.endsWith('/emby')) {
|
||||
serverUrl = serverUrl.slice(0, -5); // Remove '/emby'
|
||||
}
|
||||
|
||||
this.serverUrl = serverUrl;
|
||||
this.apiKey = config.ApiKey;
|
||||
this.userId = config.UserId;
|
||||
@@ -82,6 +101,11 @@ export class EmbyClient {
|
||||
console.log('[EmbyClient] constructor - ApiKey:', this.apiKey);
|
||||
console.log('[EmbyClient] constructor - UserId:', this.userId);
|
||||
console.log('[EmbyClient] constructor - AuthToken:', this.authToken);
|
||||
console.log('[EmbyClient] constructor - Advanced Options:', {
|
||||
removeEmbyPrefix: this.removeEmbyPrefix,
|
||||
appendMediaSourceId: this.appendMediaSourceId,
|
||||
transcodeMp4: this.transcodeMp4,
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureAuthenticated(): Promise<void> {
|
||||
@@ -437,11 +461,91 @@ export class EmbyClient {
|
||||
return `${this.serverUrl}/Items/${itemId}/Images/${imageType}${queryString ? '?' + queryString : ''}`;
|
||||
}
|
||||
|
||||
getStreamUrl(itemId: string, direct: boolean = true): string {
|
||||
if (direct) {
|
||||
return `${this.serverUrl}/Videos/${itemId}/stream?Static=true&api_key=${this.apiKey || this.authToken}`;
|
||||
/**
|
||||
* 获取 PlaybackInfo 以获取 MediaSourceId
|
||||
*/
|
||||
async getPlaybackInfo(itemId: string): Promise<{ MediaSourceId?: string }> {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error('未配置 Emby 用户 ID');
|
||||
}
|
||||
return `${this.serverUrl}/Videos/${itemId}/master.m3u8?api_key=${this.apiKey || this.authToken}`;
|
||||
|
||||
const token = this.apiKey || this.authToken;
|
||||
const url = `${this.serverUrl}/Items/${itemId}/PlaybackInfo?UserId=${this.userId}${token ? `&api_key=${token}` : ''}`;
|
||||
|
||||
console.log('[EmbyClient] getPlaybackInfo - URL:', url);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
DeviceProfile: {
|
||||
MaxStreamingBitrate: 120000000,
|
||||
MaxStaticBitrate: 100000000,
|
||||
MusicStreamingTranscodingBitrate: 384000,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('[EmbyClient] getPlaybackInfo failed:', response.status);
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const mediaSourceId = data.MediaSources?.[0]?.Id;
|
||||
|
||||
console.log('[EmbyClient] getPlaybackInfo - MediaSourceId:', mediaSourceId);
|
||||
|
||||
return { MediaSourceId: mediaSourceId };
|
||||
} catch (error) {
|
||||
console.error('[EmbyClient] getPlaybackInfo error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async getStreamUrl(itemId: string, direct: boolean = true): Promise<string> {
|
||||
const token = this.apiKey || this.authToken;
|
||||
let url: string;
|
||||
|
||||
console.log('[EmbyClient] getStreamUrl - itemId:', itemId, 'direct:', direct);
|
||||
console.log('[EmbyClient] getStreamUrl - Options:', {
|
||||
appendMediaSourceId: this.appendMediaSourceId,
|
||||
transcodeMp4: this.transcodeMp4,
|
||||
});
|
||||
|
||||
if (direct) {
|
||||
// 选项3: 转码mp4
|
||||
if (this.transcodeMp4) {
|
||||
url = `${this.serverUrl}/Videos/${itemId}/stream.mp4?api_key=${token}`;
|
||||
} else {
|
||||
url = `${this.serverUrl}/Videos/${itemId}/stream?Static=true&api_key=${token}`;
|
||||
}
|
||||
|
||||
// 选项2: 拼接MediaSourceId参数
|
||||
if (this.appendMediaSourceId) {
|
||||
console.log('[EmbyClient] getStreamUrl - Attempting to get MediaSourceId...');
|
||||
try {
|
||||
const playbackInfo = await this.getPlaybackInfo(itemId);
|
||||
if (playbackInfo.MediaSourceId) {
|
||||
url += `&MediaSourceId=${playbackInfo.MediaSourceId}`;
|
||||
console.log('[EmbyClient] getStreamUrl - MediaSourceId appended:', playbackInfo.MediaSourceId);
|
||||
} else {
|
||||
console.log('[EmbyClient] getStreamUrl - No MediaSourceId returned from PlaybackInfo');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EmbyClient] Failed to get MediaSourceId:', error);
|
||||
// 继续使用不带 MediaSourceId 的 URL
|
||||
}
|
||||
}
|
||||
} else {
|
||||
url = `${this.serverUrl}/Videos/${itemId}/master.m3u8?api_key=${token}`;
|
||||
}
|
||||
|
||||
console.log('[EmbyClient] getStreamUrl - Final URL:', url);
|
||||
return url;
|
||||
}
|
||||
|
||||
getSubtitles(item: EmbyItem): Array<{ url: string; language: string; label: string }> {
|
||||
|
||||
Reference in New Issue
Block a user