JellyfinApi.ets 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. import { http } from '@kit.NetworkKit';
  2. import { PreferencesUtil } from '@pura/harmony-utils';
  3. import { WebDavAccount } from '../../viewmodel/WebDavAccount';
  4. import { ServerLogUtil } from '../util/ServerLogUtil';
  5. const TAG = 'heanup JellyfinApi';
  6. const CLIENT_NAME = 'TTMusic';
  7. const CLIENT_VERSION = '1.0.0';
  8. const DEVICE_NAME = 'HarmonyOS';
  9. const DEVICE_ID_KEY = 'jellyfin_device_id';
  10. interface JellyfinAuthResponse {
  11. AccessToken?: string;
  12. User?: JellyfinUser;
  13. }
  14. interface JellyfinUser {
  15. Id?: string;
  16. Name?: string;
  17. }
  18. interface JellyfinAuthContext {
  19. token: string;
  20. userId: string;
  21. deviceId: string;
  22. userName?: string;
  23. }
  24. interface JellyfinItemImageTags {
  25. Primary?: string;
  26. }
  27. interface JellyfinItem {
  28. Id?: string;
  29. Name?: string;
  30. Type?: string;
  31. Album?: string;
  32. AlbumId?: string;
  33. Artists?: string[];
  34. ArtistItems?: Array<JellyfinPerson>;
  35. AlbumArtists?: Array<JellyfinPerson>;
  36. RunTimeTicks?: number;
  37. ProductionYear?: number;
  38. IndexNumber?: number;
  39. ImageTags?: JellyfinItemImageTags;
  40. MediaSources?: Array<JellyfinMediaSource>;
  41. }
  42. interface JellyfinItemsResponse {
  43. Items?: JellyfinItem[];
  44. }
  45. export interface JellyfinPagedResponse<T> {
  46. items: T[];
  47. nextStart: number | null;
  48. }
  49. interface JellyfinPerson {
  50. Id?: string;
  51. Name?: string;
  52. }
  53. interface JellyfinMediaStream {
  54. Type?: string;
  55. BitRate?: number;
  56. SampleRate?: number;
  57. Channels?: number;
  58. }
  59. interface JellyfinMediaSource {
  60. Size?: number;
  61. Container?: string;
  62. MediaStreams?: Array<JellyfinMediaStream>;
  63. }
  64. interface JellyfinAuthRequestBody {
  65. Username: string;
  66. Pw: string;
  67. }
  68. interface JellyfinAuthHeaders {
  69. Authorization: string;
  70. 'X-Emby-Token': string;
  71. Accept: string;
  72. }
  73. interface JellyfinLoginHeaders {
  74. 'Content-Type': string;
  75. Accept: string;
  76. Authorization: string;
  77. }
  78. class QueryParam {
  79. key: string;
  80. value: string;
  81. constructor(key: string, value: string) {
  82. this.key = key;
  83. this.value = value;
  84. }
  85. }
  86. export interface JellyfinArtist {
  87. id: string;
  88. name: string;
  89. albumCount?: number;
  90. }
  91. export interface JellyfinAlbum {
  92. id: string;
  93. name: string;
  94. artist?: string;
  95. songCount?: number;
  96. year?: number;
  97. }
  98. export interface JellyfinSong {
  99. id: string;
  100. title: string;
  101. album?: string;
  102. albumId?: string;
  103. artist?: string;
  104. artistId?: string;
  105. durationSeconds?: number;
  106. size?: number;
  107. suffix?: string;
  108. bitRate?: number;
  109. sampleRate?: number;
  110. track?: number;
  111. year?: number;
  112. }
  113. export class JellyfinApi {
  114. private authCache: Map<string, JellyfinAuthContext> = new Map();
  115. async getArtists(account: WebDavAccount): Promise<JellyfinArtist[]> {
  116. const params: Array<QueryParam> = [
  117. new QueryParam('SortBy', 'SortName'),
  118. new QueryParam('SortOrder', 'Ascending')
  119. ];
  120. const response = await this.get<JellyfinItemsResponse>(account, '/Artists', params);
  121. const items = response.Items ?? [];
  122. return items
  123. .filter(item => item.Id && item.Name)
  124. .map(item => {
  125. const artist: JellyfinArtist = {
  126. id: item.Id as string,
  127. name: item.Name as string
  128. };
  129. return artist;
  130. });
  131. }
  132. async getArtistAlbums(account: WebDavAccount, artistId: string): Promise<JellyfinAlbum[]> {
  133. const auth = await this.ensureAuth(account);
  134. const params: Array<QueryParam> = [
  135. new QueryParam('IncludeItemTypes', 'MusicAlbum'),
  136. new QueryParam('Recursive', 'true'),
  137. new QueryParam('SortBy', 'SortName'),
  138. new QueryParam('SortOrder', 'Ascending'),
  139. new QueryParam('ArtistIds', artistId)
  140. ];
  141. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  142. const items = response.Items ?? [];
  143. return items
  144. .filter(item => item.Id && item.Name)
  145. .map(item => {
  146. const album: JellyfinAlbum = {
  147. id: item.Id as string,
  148. name: item.Name as string,
  149. artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
  150. year: item.ProductionYear
  151. };
  152. return album;
  153. });
  154. }
  155. async getAlbums(account: WebDavAccount): Promise<JellyfinAlbum[]> {
  156. const auth = await this.ensureAuth(account);
  157. const params: Array<QueryParam> = [
  158. new QueryParam('IncludeItemTypes', 'MusicAlbum'),
  159. new QueryParam('Recursive', 'true'),
  160. new QueryParam('SortBy', 'SortName'),
  161. new QueryParam('SortOrder', 'Ascending')
  162. ];
  163. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  164. const items = response.Items ?? [];
  165. return items
  166. .filter(item => item.Id && item.Name)
  167. .map(item => {
  168. const album: JellyfinAlbum = {
  169. id: item.Id as string,
  170. name: item.Name as string,
  171. artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
  172. year: item.ProductionYear
  173. };
  174. return album;
  175. });
  176. }
  177. async getAlbum(account: WebDavAccount, albumId: string): Promise<JellyfinAlbum | null> {
  178. const auth = await this.ensureAuth(account);
  179. const item = await this.get<JellyfinItem>(account, `/Users/${auth.userId}/Items/${albumId}`);
  180. if (!item || !item.Id || !item.Name) {
  181. return null;
  182. }
  183. const album: JellyfinAlbum = {
  184. id: item.Id,
  185. name: item.Name,
  186. artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
  187. year: item.ProductionYear
  188. };
  189. return album;
  190. }
  191. async getAlbumSongs(account: WebDavAccount, albumId: string): Promise<JellyfinSong[]> {
  192. const auth = await this.ensureAuth(account);
  193. const params: Array<QueryParam> = [
  194. new QueryParam('IncludeItemTypes', 'Audio'),
  195. new QueryParam('ParentId', albumId),
  196. new QueryParam('SortBy', 'ParentIndexNumber,IndexNumber,SortName'),
  197. new QueryParam('SortOrder', 'Ascending'),
  198. new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
  199. ];
  200. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  201. const items = response.Items ?? [];
  202. const songs: JellyfinSong[] = [];
  203. for (let i = 0; i < items.length; i++) {
  204. const song = this.toSong(items[i]);
  205. if (song) {
  206. songs.push(song);
  207. }
  208. }
  209. return songs;
  210. }
  211. async getArtistSongs(account: WebDavAccount, artistId: string, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
  212. const auth = await this.ensureAuth(account);
  213. const params: Array<QueryParam> = [
  214. new QueryParam('IncludeItemTypes', 'Audio'),
  215. new QueryParam('Recursive', 'true'),
  216. new QueryParam('SortBy', 'SortName'),
  217. new QueryParam('SortOrder', 'Ascending'),
  218. new QueryParam('ArtistIds', artistId),
  219. new QueryParam('StartIndex', startIndex.toString()),
  220. new QueryParam('Limit', limit.toString()),
  221. new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
  222. ];
  223. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  224. const items = response.Items ?? [];
  225. const songs: JellyfinSong[] = [];
  226. for (let i = 0; i < items.length; i++) {
  227. const song = this.toSong(items[i]);
  228. if (song) {
  229. songs.push(song);
  230. }
  231. }
  232. const nextStart = items.length < limit ? null : startIndex + items.length;
  233. const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
  234. return result;
  235. }
  236. async getSongsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
  237. const auth = await this.ensureAuth(account);
  238. const params: Array<QueryParam> = [
  239. new QueryParam('IncludeItemTypes', 'Audio'),
  240. new QueryParam('Recursive', 'true'),
  241. new QueryParam('SortBy', 'SortName'),
  242. new QueryParam('SortOrder', 'Ascending'),
  243. new QueryParam('StartIndex', startIndex.toString()),
  244. new QueryParam('Limit', limit.toString()),
  245. new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
  246. ];
  247. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  248. const items = response.Items ?? [];
  249. const songs: JellyfinSong[] = [];
  250. for (let i = 0; i < items.length; i++) {
  251. const song = this.toSong(items[i]);
  252. if (song) {
  253. songs.push(song);
  254. }
  255. }
  256. const nextStart = items.length < limit ? null : startIndex + items.length;
  257. const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
  258. return result;
  259. }
  260. async getArtistsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinArtist>> {
  261. const params: Array<QueryParam> = [
  262. new QueryParam('SortBy', 'SortName'),
  263. new QueryParam('SortOrder', 'Ascending'),
  264. new QueryParam('StartIndex', startIndex.toString()),
  265. new QueryParam('Limit', limit.toString())
  266. ];
  267. const response = await this.get<JellyfinItemsResponse>(account, '/Artists', params);
  268. const items = response.Items ?? [];
  269. const artists: JellyfinArtist[] = items
  270. .filter(item => item.Id && item.Name)
  271. .map(item => {
  272. const artist: JellyfinArtist = {
  273. id: item.Id as string,
  274. name: item.Name as string
  275. };
  276. return artist;
  277. });
  278. const nextStart = items.length < limit ? null : startIndex + items.length;
  279. const result: JellyfinPagedResponse<JellyfinArtist> = { items: artists, nextStart: nextStart };
  280. return result;
  281. }
  282. async getAlbumsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinAlbum>> {
  283. const auth = await this.ensureAuth(account);
  284. const params: Array<QueryParam> = [
  285. new QueryParam('IncludeItemTypes', 'MusicAlbum'),
  286. new QueryParam('Recursive', 'true'),
  287. new QueryParam('SortBy', 'SortName'),
  288. new QueryParam('SortOrder', 'Ascending'),
  289. new QueryParam('StartIndex', startIndex.toString()),
  290. new QueryParam('Limit', limit.toString())
  291. ];
  292. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  293. const items = response.Items ?? [];
  294. const albums: JellyfinAlbum[] = items
  295. .filter(item => item.Id && item.Name)
  296. .map(item => {
  297. const album: JellyfinAlbum = {
  298. id: item.Id as string,
  299. name: item.Name as string,
  300. artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
  301. year: item.ProductionYear
  302. };
  303. return album;
  304. });
  305. const nextStart = items.length < limit ? null : startIndex + items.length;
  306. const result: JellyfinPagedResponse<JellyfinAlbum> = { items: albums, nextStart: nextStart };
  307. return result;
  308. }
  309. async searchSongs(account: WebDavAccount, keyword: string, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
  310. const auth = await this.ensureAuth(account);
  311. const params: Array<QueryParam> = [
  312. new QueryParam('IncludeItemTypes', 'Audio'),
  313. new QueryParam('Recursive', 'true'),
  314. new QueryParam('SearchTerm', keyword),
  315. new QueryParam('SortBy', 'SortName'),
  316. new QueryParam('SortOrder', 'Ascending'),
  317. new QueryParam('StartIndex', startIndex.toString()),
  318. new QueryParam('Limit', limit.toString()),
  319. new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
  320. ];
  321. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  322. const items = response.Items ?? [];
  323. const songs: JellyfinSong[] = [];
  324. for (let i = 0; i < items.length; i++) {
  325. const song = this.toSong(items[i]);
  326. if (song) {
  327. songs.push(song);
  328. }
  329. }
  330. const nextStart = items.length < limit ? null : startIndex + items.length;
  331. const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
  332. return result;
  333. }
  334. async getAllSongs(account: WebDavAccount): Promise<JellyfinSong[]> {
  335. const auth = await this.ensureAuth(account);
  336. const params: Array<QueryParam> = [
  337. new QueryParam('IncludeItemTypes', 'Audio'),
  338. new QueryParam('Recursive', 'true'),
  339. new QueryParam('SortBy', 'SortName'),
  340. new QueryParam('SortOrder', 'Ascending'),
  341. new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
  342. ];
  343. const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
  344. const items = response.Items ?? [];
  345. const songs: JellyfinSong[] = [];
  346. const seenIds = new Set<string>();
  347. const seenComposite = new Set<string>();
  348. for (let i = 0; i < items.length; i++) {
  349. const itemId = items[i].Id as string | undefined;
  350. if (!itemId || seenIds.has(itemId)) {
  351. continue;
  352. }
  353. const song = this.toSong(items[i]);
  354. if (song) {
  355. const compositeKey = `${song.title ?? ''}__${song.artist ?? ''}__${song.album ?? ''}__${song.durationSeconds ?? ''}`;
  356. if (seenComposite.has(compositeKey)) {
  357. continue;
  358. }
  359. songs.push(song);
  360. seenIds.add(song.id);
  361. seenComposite.add(compositeKey);
  362. }
  363. }
  364. return songs;
  365. }
  366. async buildStreamUrl(account: WebDavAccount, itemId: string): Promise<string> {
  367. const auth = await this.ensureAuth(account);
  368. const baseUrl = this.buildBaseUrl(account);
  369. const apiKey = encodeURIComponent(auth.token);
  370. return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Download?api_key=${apiKey}`;
  371. }
  372. async buildPrimaryImageUrl(account: WebDavAccount, itemId: string, width: number = 600, height: number = 600): Promise<string> {
  373. const auth = await this.ensureAuth(account);
  374. const baseUrl = this.buildBaseUrl(account);
  375. const apiKey = encodeURIComponent(auth.token);
  376. return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Primary?fillHeight=${height}&fillWidth=${width}&quality=90&api_key=${apiKey}`;
  377. }
  378. async getAuthHeaders(account: WebDavAccount): Promise<Map<string, string>> {
  379. const auth = await this.ensureAuth(account);
  380. return this.buildAuthHeaderMap(auth);
  381. }
  382. private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>): Promise<T> {
  383. return this.request<T>(account, http.RequestMethod.GET, path, params);
  384. }
  385. private async request<T>(
  386. account: WebDavAccount,
  387. method: http.RequestMethod,
  388. path: string,
  389. params?: Array<QueryParam>,
  390. body?: object,
  391. retry: boolean = true
  392. ): Promise<T> {
  393. const httpRequest = http.createHttp();
  394. try {
  395. const auth = await this.ensureAuth(account);
  396. const query = this.buildQueryString(params);
  397. const url = `${this.buildBaseUrl(account)}${path}${query ? `?${query}` : ''}`;
  398. void ServerLogUtil.info(TAG, `${method} ${url}`);
  399. const response = await httpRequest.request(url, {
  400. method,
  401. connectTimeout: 10000,
  402. readTimeout: 15000,
  403. expectDataType: http.HttpDataType.STRING,
  404. header: this.buildAuthHeaderObject(auth),
  405. extraData: body ? JSON.stringify(body) : undefined
  406. });
  407. if (response.responseCode === 401 && retry) {
  408. this.invalidateAuth(account);
  409. void ServerLogUtil.warn(TAG, `401 重试: ${url}`);
  410. return this.request<T>(account, method, path, params, body, false);
  411. }
  412. if (response.responseCode < 200 || response.responseCode >= 300) {
  413. throw new Error(`Jellyfin API 请求失败: HTTP ${response.responseCode}`);
  414. }
  415. return JSON.parse(response.result as string) as T;
  416. } finally {
  417. httpRequest.destroy();
  418. }
  419. }
  420. private async ensureAuth(account: WebDavAccount): Promise<JellyfinAuthContext> {
  421. const key = this.getAccountKey(account);
  422. const cached = this.authCache.get(key);
  423. if (cached) {
  424. return cached;
  425. }
  426. const auth = await this.login(account);
  427. this.authCache.set(key, auth);
  428. return auth;
  429. }
  430. private invalidateAuth(account: WebDavAccount): void {
  431. const key = this.getAccountKey(account);
  432. this.authCache.delete(key);
  433. }
  434. private async login(account: WebDavAccount): Promise<JellyfinAuthContext> {
  435. const httpRequest = http.createHttp();
  436. try {
  437. const url = `${this.buildBaseUrl(account)}/Users/AuthenticateByName`;
  438. const deviceId = this.ensureDeviceId();
  439. const headers: JellyfinLoginHeaders = {
  440. 'Content-Type': 'application/json',
  441. 'Accept': 'application/json',
  442. 'Authorization': this.buildLoginHeader(deviceId)
  443. };
  444. const body: JellyfinAuthRequestBody = {
  445. Username: account.account ?? '',
  446. Pw: account.password ?? ''
  447. };
  448. const response = await httpRequest.request(url, {
  449. method: http.RequestMethod.POST,
  450. connectTimeout: 10000,
  451. readTimeout: 15000,
  452. expectDataType: http.HttpDataType.STRING,
  453. header: headers,
  454. extraData: JSON.stringify(body)
  455. });
  456. if (response.responseCode < 200 || response.responseCode >= 300) {
  457. throw new Error(`Jellyfin 登录失败: HTTP ${response.responseCode}`);
  458. }
  459. const payload = JSON.parse(response.result as string) as JellyfinAuthResponse;
  460. const token = payload.AccessToken ?? '';
  461. const userId = payload.User?.Id ?? '';
  462. if (!token || !userId) {
  463. throw new Error('Jellyfin 登录返回缺少 AccessToken 或 UserId');
  464. }
  465. void ServerLogUtil.info(TAG, `Jellyfin 登录成功 user=${payload.User?.Name ?? ''}`);
  466. return {
  467. token,
  468. userId,
  469. deviceId,
  470. userName: payload.User?.Name
  471. };
  472. } finally {
  473. httpRequest.destroy();
  474. }
  475. }
  476. private buildBaseUrl(account: WebDavAccount): string {
  477. const protocol = account.enableHttps ? 'https' : 'http';
  478. const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
  479. const portPart = account.port && ((protocol === 'https' && account.port !== 443) || (protocol === 'http' && account.port !== 80))
  480. ? `:${account.port}`
  481. : '';
  482. const basePath = this.normalizeBasePath(account.jellyfinBasePath);
  483. return `${protocol}://${host}${portPart}${basePath}`;
  484. }
  485. private normalizeBasePath(path?: string): string {
  486. if (!path || path.trim().length === 0) {
  487. return '';
  488. }
  489. let normalized = path.trim();
  490. if (!normalized.startsWith('/')) {
  491. normalized = `/${normalized}`;
  492. }
  493. if (normalized.length > 1 && normalized.endsWith('/')) {
  494. normalized = normalized.slice(0, -1);
  495. }
  496. return normalized === '/' ? '' : normalized;
  497. }
  498. private buildAuthHeaderObject(auth: JellyfinAuthContext): JellyfinAuthHeaders {
  499. const headers: JellyfinAuthHeaders = {
  500. 'Authorization': this.buildAuthorizationHeader(auth),
  501. 'X-Emby-Token': auth.token,
  502. 'Accept': 'application/json'
  503. };
  504. return headers;
  505. }
  506. private buildAuthHeaderMap(auth: JellyfinAuthContext): Map<string, string> {
  507. const headers = new Map<string, string>();
  508. headers.set('Authorization', this.buildAuthorizationHeader(auth));
  509. headers.set('X-Emby-Token', auth.token);
  510. headers.set('Accept', 'application/json');
  511. return headers;
  512. }
  513. private buildAuthorizationHeader(auth: JellyfinAuthContext): string {
  514. return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${auth.deviceId}", Version="${CLIENT_VERSION}", Token="${auth.token}"`;
  515. }
  516. private buildLoginHeader(deviceId: string): string {
  517. return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${deviceId}", Version="${CLIENT_VERSION}"`;
  518. }
  519. private ensureDeviceId(): string {
  520. let deviceId = PreferencesUtil.getStringSync(DEVICE_ID_KEY, '');
  521. if (!deviceId || deviceId.length === 0) {
  522. deviceId = `ttmusic-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
  523. PreferencesUtil.putSync(DEVICE_ID_KEY, deviceId);
  524. }
  525. return deviceId;
  526. }
  527. private buildQueryString(params?: Array<QueryParam>): string {
  528. if (!params) {
  529. return '';
  530. }
  531. const parts: string[] = [];
  532. for (let i = 0; i < params.length; i++) {
  533. const param = params[i];
  534. if (!param.value || param.value.length === 0) {
  535. continue;
  536. }
  537. parts.push(`${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`);
  538. }
  539. return parts.join('&');
  540. }
  541. private getAccountKey(account: WebDavAccount): string {
  542. return `${account.id ?? ''}|${account.host}|${account.port}|${account.account}`;
  543. }
  544. private toSong(item: JellyfinItem): JellyfinSong | null {
  545. if (!item.Id || !item.Name) {
  546. return null;
  547. }
  548. const mediaSource = item.MediaSources && item.MediaSources.length > 0 ? item.MediaSources[0] : undefined;
  549. const audioStream = mediaSource?.MediaStreams?.find(stream => stream.Type === 'Audio');
  550. const durationSeconds = item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined;
  551. const song: JellyfinSong = {
  552. id: item.Id,
  553. title: item.Name,
  554. album: item.Album,
  555. albumId: item.AlbumId,
  556. artist: item.Artists?.[0] ?? item.ArtistItems?.[0]?.Name,
  557. artistId: item.ArtistItems?.[0]?.Id,
  558. durationSeconds,
  559. size: mediaSource?.Size,
  560. suffix: mediaSource?.Container,
  561. bitRate: audioStream?.BitRate,
  562. sampleRate: audioStream?.SampleRate,
  563. track: item.IndexNumber,
  564. year: item.ProductionYear
  565. };
  566. return song;
  567. }
  568. }
  569. export const jellyfinApi = new JellyfinApi();