@@ -17,19 +17,22 @@ import type {
1717 * 获取请求头
1818 */
1919const getHeaders = ( config : StreamingServerConfig ) : HeadersInit => {
20- const headers : HeadersInit = {
21- "Content-Type" : "application/json" ,
22- "X-Emby- Client" : " SPlayer",
23- "X-Emby-Client- Version" : " 1.0.0",
24- "X-Emby- Device-Name" : " SPlayer Web",
25- "X-Emby-Device-Id" : " splayer-web-client",
26- } ;
20+ // Jellyfin 需要 X-Emby-Authorization 头
21+ const authParts = [
22+ `MediaBrowser Client=" SPlayer"` ,
23+ ` Version=" 1.0.0"` ,
24+ ` Device=" SPlayer Web"` ,
25+ `DeviceId=" splayer-web-client"` ,
26+ ] ;
2727
2828 if ( config . accessToken ) {
29- headers [ "X-Emby- Token" ] = config . accessToken ;
29+ authParts . push ( ` Token=" ${ config . accessToken } "` ) ;
3030 }
3131
32- return headers ;
32+ return {
33+ "Content-Type" : "application/json" ,
34+ "X-Emby-Authorization" : authParts . join ( ", " ) ,
35+ } ;
3336} ;
3437
3538/**
@@ -76,11 +79,18 @@ export const getImageUrl = (
7679 config : StreamingServerConfig ,
7780 itemId : string ,
7881 imageType : "Primary" | "Backdrop" | "Banner" = "Primary" ,
79- maxWidth : number = 300 ,
82+ maxHeight ?: number ,
83+ tag ?: string ,
8084) : string => {
8185 if ( ! itemId ) return "" ;
8286 const baseUrl = config . url . endsWith ( "/" ) ? config . url . slice ( 0 , - 1 ) : config . url ;
83- return `${ baseUrl } /Items/${ itemId } /Images/${ imageType } ?maxWidth=${ maxWidth } &quality=90` ;
87+ const params = new URLSearchParams ( {
88+ quality : "100" ,
89+ } ) ;
90+ if ( maxHeight ) params . append ( "maxHeight" , maxHeight . toString ( ) ) ;
91+ if ( tag ) params . append ( "tag" , tag ) ;
92+ if ( config . accessToken ) params . append ( "api_key" , config . accessToken ) ;
93+ return `${ baseUrl } /Items/${ itemId } /Images/${ imageType } ?${ params . toString ( ) } ` ;
8494} ;
8595
8696/**
@@ -89,8 +99,18 @@ export const getImageUrl = (
8999export const getAudioStreamUrl = ( config : StreamingServerConfig , itemId : string ) : string => {
90100 const baseUrl = config . url . endsWith ( "/" ) ? config . url . slice ( 0 , - 1 ) : config . url ;
91101 const params = new URLSearchParams ( {
92- static : "true" ,
102+ UserId : config . userId || "" ,
103+ DeviceId : "splayer-web-client" ,
104+ MaxStreamingBitrate : "140000000" , // High bitrate to prefer direct play/high quality
105+ Container : "opus,webm|opus,ts|mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg" ,
106+ TranscodingContainer : "ts" ,
107+ TranscodingProtocol : "hls" ,
108+ AudioCodec : "aac" ,
109+ PlaySessionId : Date . now ( ) . toString ( ) ,
93110 api_key : config . accessToken || "" ,
111+ StartTimeTicks : "0" ,
112+ EnableRedirection : "true" ,
113+ EnableRemoteMedia : "true" ,
94114 } ) ;
95115 return `${ baseUrl } /Audio/${ itemId } /universal?${ params . toString ( ) } ` ;
96116} ;
@@ -116,15 +136,22 @@ export const convertJellyfinSong = (
116136 config : StreamingServerConfig ,
117137) : SongType => {
118138 const artists = item . Artists ?. join ( ", " ) || item . AlbumArtist || "未知艺术家" ;
119- const albumImageId = item . AlbumId || item . Id ;
139+ const imageId = item . Id ;
140+ const imageTag = item . ImageTags ?. Primary ;
120141
121142 return {
122143 id : stringToNumericId ( item . Id ) ,
123144 originalId : item . Id ,
124145 name : item . Name ,
125146 artists,
126147 album : item . Album || "未知专辑" ,
127- cover : getImageUrl ( config , albumImageId ) ,
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+ } ,
128155 duration : item . RunTimeTicks ? Math . floor ( item . RunTimeTicks / 10000 ) : 0 , // 转换为毫秒
129156 size : 0 ,
130157 free : 0 ,
@@ -145,13 +172,20 @@ export const convertJellyfinAlbum = (
145172 config : StreamingServerConfig ,
146173) : StreamingAlbumType => {
147174 const artistId = item . AlbumArtists ?. [ 0 ] ?. Id || item . ArtistItems ?. [ 0 ] ?. Id ;
175+ const imageTag = item . ImageTags ?. Primary ;
148176
149177 return {
150178 id : item . Id ,
151179 name : item . Name ,
152180 artist : item . AlbumArtist || item . AlbumArtists ?. [ 0 ] ?. Name ,
153181 artistId,
154- cover : getImageUrl ( config , item . Id ) ,
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+ } ,
155189 songCount : item . SongCount || item . ChildCount ,
156190 year : item . ProductionYear ,
157191 serverId : config . id ,
@@ -166,10 +200,17 @@ export const convertJellyfinArtist = (
166200 item : JellyfinItem ,
167201 config : StreamingServerConfig ,
168202) : StreamingArtistType => {
203+ const imageTag = item . ImageTags ?. Primary ;
169204 return {
170205 id : item . Id ,
171206 name : item . Name ,
172- cover : getImageUrl ( config , item . Id ) ,
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+ } ,
173214 albumCount : item . ChildCount ,
174215 serverId : config . id ,
175216 serverType : config . type ,
@@ -183,11 +224,18 @@ export const convertJellyfinPlaylist = (
183224 item : JellyfinItem ,
184225 config : StreamingServerConfig ,
185226) : StreamingPlaylistType => {
227+ const imageTag = item . ImageTags ?. Primary ;
186228 return {
187229 id : item . Id ,
188230 name : item . Name ,
189231 description : item . Overview ,
190- cover : getImageUrl ( config , item . Id ) ,
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+ } ,
191239 songCount : item . ChildCount ,
192240 serverId : config . id ,
193241 serverType : config . type ,
@@ -433,6 +481,37 @@ export const getSongs = async (
433481 return result . Items . map ( ( item ) => convertJellyfinSong ( item , config ) ) ;
434482} ;
435483
484+ /**
485+ * 获取歌词
486+ */
487+ export const getLyrics = async ( config : StreamingServerConfig , itemId : string ) : Promise < string > => {
488+ if ( ! itemId ) return "" ;
489+ try {
490+ const result = await request < { Lyrics : { Text : string ; Start : number } [ ] } > (
491+ config ,
492+ `Audio/${ itemId } /Lyrics` ,
493+ ) ;
494+ if ( result && Array . isArray ( result . Lyrics ) ) {
495+ return result . Lyrics . map ( ( l ) => {
496+ const totalSeconds = l . Start / 10000000 ;
497+ const minutes = Math . floor ( totalSeconds / 60 ) ;
498+ const seconds = Math . floor ( totalSeconds % 60 ) ;
499+ const milliseconds = Math . floor ( ( totalSeconds % 1 ) * 100 ) ;
500+
501+ const mm = minutes . toString ( ) . padStart ( 2 , "0" ) ;
502+ const ss = seconds . toString ( ) . padStart ( 2 , "0" ) ;
503+ const xx = milliseconds . toString ( ) . padStart ( 2 , "0" ) ;
504+
505+ return `[${ mm } :${ ss } .${ xx } ]${ l . Text } ` ;
506+ } ) . join ( "\n" ) ;
507+ }
508+ return "" ;
509+ } catch ( error ) {
510+ console . warn ( "Failed to fetch lyrics from Jellyfin:" , error ) ;
511+ return "" ;
512+ }
513+ } ;
514+
436515export default {
437516 authenticate,
438517 ping,
@@ -446,4 +525,5 @@ export default {
446525 search,
447526 getImageUrl,
448527 getAudioStreamUrl,
528+ getLyrics,
449529} ;
0 commit comments