Skip to content

Commit 1b7f079

Browse files
committed
✨ feat: 流媒体 Emby 支持
1 parent 3a3c361 commit 1b7f079

File tree

12 files changed

+757
-74
lines changed

12 files changed

+757
-74
lines changed

src/api/streaming/emby.ts

Lines changed: 546 additions & 0 deletions
Large diffs are not rendered by default.

src/api/streaming/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * as jellyfinApi from "./jellyfin";
77

88
export { default as subsonic } from "./subsonic";
99
export { default as jellyfin } from "./jellyfin";
10+
export { default as emby } from "./emby";

src/api/streaming/jellyfin.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ export const convertJellyfinSong = (
145145
name: item.Name,
146146
artists,
147147
album: item.Album || "未知专辑",
148-
cover: getImageUrl(config, imageId, "Primary", 300, imageTag),
149-
coverSize: {
150-
s: getImageUrl(config, imageId, "Primary", 100, imageTag),
151-
m: getImageUrl(config, imageId, "Primary", 300, imageTag),
152-
l: getImageUrl(config, imageId, "Primary", 1024, imageTag),
153-
xl: getImageUrl(config, imageId, "Primary", undefined, imageTag),
154-
},
148+
cover: imageTag ? getImageUrl(config, imageId, "Primary", 300, imageTag) : "",
149+
coverSize: imageTag
150+
? {
151+
s: getImageUrl(config, imageId, "Primary", 100, imageTag),
152+
m: getImageUrl(config, imageId, "Primary", 300, imageTag),
153+
l: getImageUrl(config, imageId, "Primary", 1024, imageTag),
154+
xl: getImageUrl(config, imageId, "Primary", undefined, imageTag),
155+
}
156+
: undefined,
155157
duration: item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000) : 0, // 转换为毫秒
156158
size: 0,
157159
free: 0,
@@ -179,13 +181,15 @@ export const convertJellyfinAlbum = (
179181
name: item.Name,
180182
artist: item.AlbumArtist || item.AlbumArtists?.[0]?.Name,
181183
artistId,
182-
cover: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
183-
coverSize: {
184-
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
185-
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
186-
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
187-
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
188-
},
184+
cover: imageTag ? getImageUrl(config, item.Id, "Primary", undefined, imageTag) : "",
185+
coverSize: imageTag
186+
? {
187+
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
188+
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
189+
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
190+
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
191+
}
192+
: undefined,
189193
songCount: item.SongCount || item.ChildCount,
190194
year: item.ProductionYear,
191195
serverId: config.id,
@@ -204,13 +208,15 @@ export const convertJellyfinArtist = (
204208
return {
205209
id: item.Id,
206210
name: item.Name,
207-
cover: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
208-
coverSize: {
209-
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
210-
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
211-
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
212-
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
213-
},
211+
cover: imageTag ? getImageUrl(config, item.Id, "Primary", undefined, imageTag) : "",
212+
coverSize: imageTag
213+
? {
214+
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
215+
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
216+
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
217+
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
218+
}
219+
: undefined,
214220
albumCount: item.ChildCount,
215221
serverId: config.id,
216222
serverType: config.type,
@@ -229,13 +235,15 @@ export const convertJellyfinPlaylist = (
229235
id: item.Id,
230236
name: item.Name,
231237
description: item.Overview,
232-
cover: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
233-
coverSize: {
234-
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
235-
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
236-
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
237-
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
238-
},
238+
cover: imageTag ? getImageUrl(config, item.Id, "Primary", undefined, imageTag) : "",
239+
coverSize: imageTag
240+
? {
241+
s: getImageUrl(config, item.Id, "Primary", 100, imageTag),
242+
m: getImageUrl(config, item.Id, "Primary", 300, imageTag),
243+
l: getImageUrl(config, item.Id, "Primary", 1024, imageTag),
244+
xl: getImageUrl(config, item.Id, "Primary", undefined, imageTag),
245+
}
246+
: undefined,
239247
songCount: item.ChildCount,
240248
serverId: config.id,
241249
serverType: config.type,

src/components/Modal/Setting/StreamingServerConfig.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ const serverForm = reactive({
7575
const serverTypeOptions = [
7676
{ label: "Navidrome", value: "navidrome" },
7777
{ label: "Jellyfin", value: "jellyfin" },
78+
{ label: "Emby", value: "emby" },
79+
{ label: "Subsonic", value: "subsonic" },
7880
{ label: "OpenSubsonic", value: "opensubsonic" },
7981
];
8082

src/components/Setting/StreamingSetting.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ const getServerTypeLabel = (type: StreamingServerType): string => {
114114
const labels: Record<StreamingServerType, string> = {
115115
navidrome: "Navidrome",
116116
jellyfin: "Jellyfin",
117+
emby: "Emby",
118+
subsonic: "Subsonic", // 兼容
117119
opensubsonic: "OpenSubsonic",
118120
};
119121
return labels[type] || type;

src/stores/streaming.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
StreamingPlaylistType,
1212
} from "@/types/streaming";
1313
import { SongType } from "@/types/main";
14-
import { subsonic, jellyfin } from "@/api/streaming";
14+
import { subsonic, jellyfin, emby } from "@/api/streaming";
1515
import localforage from "localforage";
1616

1717
// 创建存储实例
@@ -165,6 +165,18 @@ const createStreamingStore = () => {
165165
serverName: config.name,
166166
serverVersion: pingResult.version,
167167
};
168+
} else if (config.type === "emby") {
169+
// Emby 需要先认证
170+
const authResult = await emby.authenticate(config);
171+
config.accessToken = authResult.accessToken;
172+
config.userId = authResult.userId;
173+
174+
const pingResult = await emby.ping(config);
175+
return {
176+
connected: true,
177+
serverName: config.name,
178+
serverVersion: pingResult.version,
179+
};
168180
} else {
169181
// Subsonic API (Navidrome / OpenSubsonic)
170182
const pingResult = await subsonic.ping(config);
@@ -200,8 +212,8 @@ const createStreamingStore = () => {
200212
activeServerId.value = serverId;
201213
server.lastConnected = Date.now();
202214

203-
// 如果是 Jellyfin,保存认证信息
204-
if (server.type === "jellyfin" && server.accessToken) {
215+
// 如果是 Jellyfin 或 Emby,保存认证信息
216+
if ((server.type === "jellyfin" || server.type === "emby") && server.accessToken) {
205217
await updateServer(serverId, {
206218
accessToken: server.accessToken,
207219
userId: server.userId,
@@ -258,6 +270,8 @@ const createStreamingStore = () => {
258270

259271
if (server.type === "jellyfin") {
260272
result = await jellyfin.getRandomSongs(server, count);
273+
} else if (server.type === "emby") {
274+
result = await emby.getRandomSongs(server, count);
261275
} else {
262276
result = await subsonic.getRandomSongs(server, count);
263277
}
@@ -292,6 +306,8 @@ const createStreamingStore = () => {
292306

293307
if (server.type === "jellyfin") {
294308
result = await jellyfin.getSongs(server, offset, size);
309+
} else if (server.type === "emby") {
310+
result = await emby.getSongs(server, offset, size);
295311
} else {
296312
result = await subsonic.getSongs(server, offset, size);
297313
}
@@ -323,6 +339,8 @@ const createStreamingStore = () => {
323339

324340
if (server.type === "jellyfin") {
325341
result = await jellyfin.getArtists(server);
342+
} else if (server.type === "emby") {
343+
result = await emby.getArtists(server);
326344
} else {
327345
result = await subsonic.getArtists(server);
328346
}
@@ -350,6 +368,8 @@ const createStreamingStore = () => {
350368

351369
if (server.type === "jellyfin") {
352370
result = await jellyfin.getAlbums(server);
371+
} else if (server.type === "emby") {
372+
result = await emby.getAlbums(server);
353373
} else {
354374
result = await subsonic.getAlbumList(server, "alphabeticalByName");
355375
}
@@ -377,6 +397,8 @@ const createStreamingStore = () => {
377397

378398
if (server.type === "jellyfin") {
379399
result = await jellyfin.getPlaylists(server);
400+
} else if (server.type === "emby") {
401+
result = await emby.getPlaylists(server);
380402
} else {
381403
result = await subsonic.getPlaylists(server);
382404
}
@@ -401,6 +423,8 @@ const createStreamingStore = () => {
401423
try {
402424
if (server.type === "jellyfin") {
403425
return await jellyfin.getAlbumItems(server, albumId);
426+
} else if (server.type === "emby") {
427+
return await emby.getAlbumItems(server, albumId);
404428
} else {
405429
const result = await subsonic.getAlbum(server, albumId);
406430
return result.songs;
@@ -421,6 +445,8 @@ const createStreamingStore = () => {
421445
try {
422446
if (server.type === "jellyfin") {
423447
return await jellyfin.getPlaylistItems(server, playlistId);
448+
} else if (server.type === "emby") {
449+
return await emby.getPlaylistItems(server, playlistId);
424450
} else {
425451
const result = await subsonic.getPlaylist(server, playlistId);
426452
return result.songs;
@@ -449,6 +475,8 @@ const createStreamingStore = () => {
449475
try {
450476
if (server.type === "jellyfin") {
451477
return await jellyfin.search(server, query);
478+
} else if (server.type === "emby") {
479+
return await emby.search(server, query);
452480
} else {
453481
return await subsonic.search(server, query);
454482
}
@@ -468,6 +496,8 @@ const createStreamingStore = () => {
468496
try {
469497
if (server.type === "jellyfin" && song.originalId) {
470498
return await jellyfin.getLyrics(server, song.originalId);
499+
} else if (server.type === "emby" && song.originalId) {
500+
return await emby.getLyrics(server, song.originalId);
471501
} else {
472502
// 优先使用 ID 获取
473503
if (song.originalId) {
@@ -495,6 +525,10 @@ const createStreamingStore = () => {
495525
return jellyfin.getAudioStreamUrl(server, song.originalId);
496526
}
497527

528+
if (server.type === "emby" && server.accessToken && song.originalId) {
529+
return emby.getAudioStreamUrl(server, song.originalId);
530+
}
531+
498532
return song.streamUrl || "";
499533
};
500534

src/types/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export type SongType = {
106106
/** 原始 ID(流媒体服务器的 ID) */
107107
originalId?: string;
108108
/** 流媒体服务器类型 */
109-
serverType?: "navidrome" | "jellyfin" | "opensubsonic";
109+
serverType?: "navidrome" | "jellyfin" | "subsonic" | "opensubsonic" | "emby";
110110
/** 流媒体服务器 ID */
111111
serverId?: string;
112112
/** 来源标记 */

src/types/streaming.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { SongType, CoverSize } from "./main";
77
/**
88
* 流媒体服务器类型
99
*/
10-
export type StreamingServerType = "navidrome" | "jellyfin" | "opensubsonic";
10+
export type StreamingServerType = "navidrome" | "jellyfin" | "subsonic" | "opensubsonic" | "emby";
1111

1212
/**
1313
* 流媒体服务器配置

src/views/Streaming/albums.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,6 @@ watch(
105105
if (albumDom) albumDom.scrollIntoView({ behavior: "smooth", block: "center" });
106106
},
107107
);
108-
109-
// 初始化加载
110-
onMounted(async () => {
111-
if (streamingStore.isConnected.value && streamingStore.songs.value.length === 0) {
112-
await streamingStore.fetchRandomSongs(200);
113-
}
114-
});
115108
</script>
116109

117110
<style lang="scss" scoped>

src/views/Streaming/artists.vue

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,6 @@ watch(
9494
if (artistDom) artistDom.scrollIntoView({ behavior: "smooth", block: "center" });
9595
},
9696
);
97-
98-
// 初始化加载
99-
onMounted(async () => {
100-
if (streamingStore.isConnected.value && streamingStore.songs.value.length === 0) {
101-
await streamingStore.fetchRandomSongs(200);
102-
}
103-
});
10497
</script>
10598

10699
<style lang="scss" scoped>

0 commit comments

Comments
 (0)