| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- import { http } from '@kit.NetworkKit';
- import { PreferencesUtil } from '@pura/harmony-utils';
- import { WebDavAccount } from '../../viewmodel/WebDavAccount';
- import { ServerLogUtil } from '../util/ServerLogUtil';
- const TAG = 'heanup JellyfinApi';
- const CLIENT_NAME = 'TTMusic';
- const CLIENT_VERSION = '1.0.0';
- const DEVICE_NAME = 'HarmonyOS';
- const DEVICE_ID_KEY = 'jellyfin_device_id';
- interface JellyfinAuthResponse {
- AccessToken?: string;
- User?: JellyfinUser;
- }
- interface JellyfinUser {
- Id?: string;
- Name?: string;
- }
- interface JellyfinAuthContext {
- token: string;
- userId: string;
- deviceId: string;
- userName?: string;
- }
- interface JellyfinItemImageTags {
- Primary?: string;
- }
- interface JellyfinItem {
- Id?: string;
- Name?: string;
- Type?: string;
- Album?: string;
- AlbumId?: string;
- Artists?: string[];
- ArtistItems?: Array<JellyfinPerson>;
- AlbumArtists?: Array<JellyfinPerson>;
- RunTimeTicks?: number;
- ProductionYear?: number;
- IndexNumber?: number;
- ImageTags?: JellyfinItemImageTags;
- MediaSources?: Array<JellyfinMediaSource>;
- }
- interface JellyfinItemsResponse {
- Items?: JellyfinItem[];
- }
- export interface JellyfinPagedResponse<T> {
- items: T[];
- nextStart: number | null;
- }
- interface JellyfinPerson {
- Id?: string;
- Name?: string;
- }
- interface JellyfinMediaStream {
- Type?: string;
- BitRate?: number;
- SampleRate?: number;
- Channels?: number;
- }
- interface JellyfinMediaSource {
- Size?: number;
- Container?: string;
- MediaStreams?: Array<JellyfinMediaStream>;
- }
- interface JellyfinLyricLine {
- Text?: string;
- Start?: number;
- }
- interface JellyfinLyricData {
- Lyrics?: JellyfinLyricLine[];
- }
- interface JellyfinAuthRequestBody {
- Username: string;
- Pw: string;
- }
- interface JellyfinAuthHeaders {
- Authorization: string;
- 'X-Emby-Token': string;
- Accept: string;
- }
- interface JellyfinLoginHeaders {
- 'Content-Type': string;
- Accept: string;
- Authorization: string;
- }
- class QueryParam {
- key: string;
- value: string;
- constructor(key: string, value: string) {
- this.key = key;
- this.value = value;
- }
- }
- export interface JellyfinArtist {
- id: string;
- name: string;
- albumCount?: number;
- songCount?: number;
- }
- export interface JellyfinAlbum {
- id: string;
- name: string;
- artist?: string;
- songCount?: number;
- year?: number;
- }
- export interface JellyfinSong {
- id: string;
- title: string;
- album?: string;
- albumId?: string;
- artist?: string;
- artistId?: string;
- durationSeconds?: number;
- size?: number;
- suffix?: string;
- bitRate?: number;
- sampleRate?: number;
- track?: number;
- year?: number;
- }
- export class JellyfinApi {
- private authCache: Map<string, JellyfinAuthContext> = new Map();
- async getArtists(account: WebDavAccount): Promise<JellyfinArtist[]> {
- const params: Array<QueryParam> = [
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, '/Artists', params);
- const items = response.Items ?? [];
- return items
- .filter(item => item.Id && item.Name)
- .map(item => {
- const artist: JellyfinArtist = {
- id: item.Id as string,
- name: item.Name as string
- };
- return artist;
- });
- }
- async getArtistAlbums(account: WebDavAccount, artistId: string): Promise<JellyfinAlbum[]> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'MusicAlbum'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('ArtistIds', artistId)
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- return items
- .filter(item => item.Id && item.Name)
- .map(item => {
- const album: JellyfinAlbum = {
- id: item.Id as string,
- name: item.Name as string,
- artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
- year: item.ProductionYear
- };
- return album;
- });
- }
- async getAlbums(account: WebDavAccount): Promise<JellyfinAlbum[]> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'MusicAlbum'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- return items
- .filter(item => item.Id && item.Name)
- .map(item => {
- const album: JellyfinAlbum = {
- id: item.Id as string,
- name: item.Name as string,
- artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
- year: item.ProductionYear
- };
- return album;
- });
- }
- async getAlbum(account: WebDavAccount, albumId: string): Promise<JellyfinAlbum | null> {
- const auth = await this.ensureAuth(account);
- const item = await this.get<JellyfinItem>(account, `/Users/${auth.userId}/Items/${albumId}`);
- if (!item || !item.Id || !item.Name) {
- return null;
- }
- const album: JellyfinAlbum = {
- id: item.Id,
- name: item.Name,
- artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
- year: item.ProductionYear
- };
- return album;
- }
- async getAlbumSongs(account: WebDavAccount, albumId: string): Promise<JellyfinSong[]> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'Audio'),
- new QueryParam('ParentId', albumId),
- new QueryParam('SortBy', 'ParentIndexNumber,IndexNumber,SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const songs: JellyfinSong[] = [];
- for (let i = 0; i < items.length; i++) {
- const song = this.toSong(items[i]);
- if (song) {
- songs.push(song);
- }
- }
- return songs;
- }
- async getArtistSongs(account: WebDavAccount, artistId: string, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'Audio'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('ArtistIds', artistId),
- new QueryParam('StartIndex', startIndex.toString()),
- new QueryParam('Limit', limit.toString()),
- new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const songs: JellyfinSong[] = [];
- for (let i = 0; i < items.length; i++) {
- const song = this.toSong(items[i]);
- if (song) {
- songs.push(song);
- }
- }
- const nextStart = items.length < limit ? null : startIndex + items.length;
- const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
- return result;
- }
- async getSongsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'Audio'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('StartIndex', startIndex.toString()),
- new QueryParam('Limit', limit.toString()),
- new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const songs: JellyfinSong[] = [];
- for (let i = 0; i < items.length; i++) {
- const song = this.toSong(items[i]);
- if (song) {
- songs.push(song);
- }
- }
- const nextStart = items.length < limit ? null : startIndex + items.length;
- const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
- return result;
- }
- async getArtistsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinArtist>> {
- const params: Array<QueryParam> = [
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('StartIndex', startIndex.toString()),
- new QueryParam('Limit', limit.toString())
- ];
- const response = await this.get<JellyfinItemsResponse>(account, '/Artists', params);
- const items = response.Items ?? [];
- const artists: JellyfinArtist[] = items
- .filter(item => item.Id && item.Name)
- .map(item => {
- const artist: JellyfinArtist = {
- id: item.Id as string,
- name: item.Name as string
- };
- return artist;
- });
- const nextStart = items.length < limit ? null : startIndex + items.length;
- const result: JellyfinPagedResponse<JellyfinArtist> = { items: artists, nextStart: nextStart };
- return result;
- }
- async getAlbumsPage(account: WebDavAccount, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinAlbum>> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'MusicAlbum'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('StartIndex', startIndex.toString()),
- new QueryParam('Limit', limit.toString())
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const albums: JellyfinAlbum[] = items
- .filter(item => item.Id && item.Name)
- .map(item => {
- const album: JellyfinAlbum = {
- id: item.Id as string,
- name: item.Name as string,
- artist: item.AlbumArtists?.[0]?.Name ?? item.Artists?.[0],
- year: item.ProductionYear
- };
- return album;
- });
- const nextStart = items.length < limit ? null : startIndex + items.length;
- const result: JellyfinPagedResponse<JellyfinAlbum> = { items: albums, nextStart: nextStart };
- return result;
- }
- async searchSongs(account: WebDavAccount, keyword: string, startIndex: number = 0, limit: number = 200): Promise<JellyfinPagedResponse<JellyfinSong>> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'Audio'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SearchTerm', keyword),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('StartIndex', startIndex.toString()),
- new QueryParam('Limit', limit.toString()),
- new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const songs: JellyfinSong[] = [];
- for (let i = 0; i < items.length; i++) {
- const song = this.toSong(items[i]);
- if (song) {
- songs.push(song);
- }
- }
- const nextStart = items.length < limit ? null : startIndex + items.length;
- const result: JellyfinPagedResponse<JellyfinSong> = { items: songs, nextStart: nextStart };
- return result;
- }
- async getAllSongs(account: WebDavAccount): Promise<JellyfinSong[]> {
- const auth = await this.ensureAuth(account);
- const params: Array<QueryParam> = [
- new QueryParam('IncludeItemTypes', 'Audio'),
- new QueryParam('Recursive', 'true'),
- new QueryParam('SortBy', 'SortName'),
- new QueryParam('SortOrder', 'Ascending'),
- new QueryParam('Fields', 'MediaSources,AudioInfo,PrimaryImageAspectRatio')
- ];
- const response = await this.get<JellyfinItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
- const items = response.Items ?? [];
- const songs: JellyfinSong[] = [];
- const seenIds = new Set<string>();
- const seenComposite = new Set<string>();
- for (let i = 0; i < items.length; i++) {
- const itemId = items[i].Id as string | undefined;
- if (!itemId || seenIds.has(itemId)) {
- continue;
- }
- const song = this.toSong(items[i]);
- if (song) {
- const compositeKey = `${song.title ?? ''}__${song.artist ?? ''}__${song.album ?? ''}__${song.durationSeconds ?? ''}`;
- if (seenComposite.has(compositeKey)) {
- continue;
- }
- songs.push(song);
- seenIds.add(song.id);
- seenComposite.add(compositeKey);
- }
- }
- return songs;
- }
- async buildStreamUrl(account: WebDavAccount, itemId: string, useStatic: boolean = false): Promise<string> {
- const auth = await this.ensureAuth(account);
- const baseUrl = this.buildBaseUrl(account);
- const apiKey = encodeURIComponent(auth.token);
- const params: string[] = [`api_key=${apiKey}`];
- if (useStatic) {
- params.push('static=true');
- }
- return `${baseUrl}/Audio/${encodeURIComponent(itemId)}/stream?${params.join('&')}`;
- }
- async buildDownloadUrl(account: WebDavAccount, itemId: string): Promise<string> {
- const auth = await this.ensureAuth(account);
- const baseUrl = this.buildBaseUrl(account);
- const apiKey = encodeURIComponent(auth.token);
- return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Download?api_key=${apiKey}`;
- }
- async canAccessUrl(account: WebDavAccount, url: string): Promise<boolean> {
- const httpRequest = http.createHttp();
- try {
- const auth = await this.ensureAuth(account);
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.HEAD,
- connectTimeout: 10000,
- readTimeout: 10000,
- expectDataType: http.HttpDataType.STRING,
- header: this.buildAuthHeaderObject(auth)
- });
- return response.responseCode === 200 || response.responseCode === 206;
- } catch (_error) {
- return false;
- } finally {
- httpRequest.destroy();
- }
- }
- async buildPrimaryImageUrl(account: WebDavAccount, itemId: string, width: number = 600, height: number = 600): Promise<string> {
- const auth = await this.ensureAuth(account);
- const baseUrl = this.buildBaseUrl(account);
- const apiKey = encodeURIComponent(auth.token);
- return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Primary?fillHeight=${height}&fillWidth=${width}&quality=90&api_key=${apiKey}`;
- }
- /**
- * 获取歌词
- * GET: /Audio/{id}/Lyrics
- * @param account WebDavAccount账号信息
- * @param itemId 歌曲ID
- * @returns 歌词文本,如果获取失败返回空字符串
- */
- async getLyric(account: WebDavAccount, itemId: string): Promise<string> {
- const httpRequest = http.createHttp();
- try {
- const auth = await this.ensureAuth(account);
- const baseUrl = this.buildBaseUrl(account);
- const url = `${baseUrl}/Audio/${encodeURIComponent(itemId)}/Lyrics`;
- void ServerLogUtil.info(TAG, `获取Jellyfin歌词: ${url}`);
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.GET,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: this.buildAuthHeaderObject(auth)
- });
- if (response.responseCode < 200 || response.responseCode >= 300) {
- void ServerLogUtil.error(TAG, `获取Jellyfin歌词失败 code=${response.responseCode}`);
- return '';
- }
- void ServerLogUtil.info(TAG, `获取Jellyfin歌词成功 code=${response.responseCode}`);
- const lyricData = JSON.parse(response.result as string) as JellyfinLyricData;
- // 转换为标准 LRC 格式
- return this.convertJellyfinLyricToLrc(lyricData);
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `获取Jellyfin歌词异常: ${err.message}`);
- return '';
- } finally {
- httpRequest.destroy();
- }
- }
- /**
- * 将Jellyfin歌词数据转换为LRC格式
- * @param lyricData Jellyfin歌词数据
- * @returns LRC格式歌词字符串
- */
- private convertJellyfinLyricToLrc(lyricData: JellyfinLyricData): string {
- if (!lyricData || !lyricData.Lyrics || !Array.isArray(lyricData.Lyrics)) {
- return '';
- }
- const lines: string[] = [];
- for (let i = 0; i < lyricData.Lyrics.length; i++) {
- const lyricLine = lyricData.Lyrics[i];
- if (lyricLine.Text && lyricLine.Start !== undefined) {
- // 将纳秒转换为毫秒,再转换为秒
- const milliseconds = Math.floor(lyricLine.Start / 1000000); // 纳秒转毫秒
- const seconds = milliseconds / 1000; // 毫秒转秒
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = (seconds % 60).toFixed(2);
- const timeTag = `[${String(minutes).padStart(2, '0')}:${remainingSeconds.padStart(5, '0')}]`;
- lines.push(`${timeTag}${lyricLine.Text}`);
- }
- }
- return lines.join('\n');
- }
- async getAuthHeaders(account: WebDavAccount): Promise<Map<string, string>> {
- const auth = await this.ensureAuth(account);
- return this.buildAuthHeaderMap(auth);
- }
- private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>): Promise<T> {
- return this.request<T>(account, http.RequestMethod.GET, path, params);
- }
- private async request<T>(
- account: WebDavAccount,
- method: http.RequestMethod,
- path: string,
- params?: Array<QueryParam>,
- body?: object,
- retry: boolean = true
- ): Promise<T> {
- const httpRequest = http.createHttp();
- try {
- const auth = await this.ensureAuth(account);
- const query = this.buildQueryString(params);
- const url = `${this.buildBaseUrl(account)}${path}${query ? `?${query}` : ''}`;
- void ServerLogUtil.info(TAG, `${method} ${url}`);
- const response = await httpRequest.request(url, {
- method,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: this.buildAuthHeaderObject(auth),
- extraData: body ? JSON.stringify(body) : undefined
- });
- if (response.responseCode === 401 && retry) {
- this.invalidateAuth(account);
- void ServerLogUtil.warn(TAG, `401 重试: ${url}`);
- return this.request<T>(account, method, path, params, body, false);
- }
- if (response.responseCode < 200 || response.responseCode >= 300) {
- throw new Error(`Jellyfin API 请求失败: HTTP ${response.responseCode}`);
- }
- return JSON.parse(response.result as string) as T;
- } finally {
- httpRequest.destroy();
- }
- }
- private async ensureAuth(account: WebDavAccount): Promise<JellyfinAuthContext> {
- const key = this.getAccountKey(account);
- const cached = this.authCache.get(key);
- if (cached) {
- return cached;
- }
- const auth = await this.login(account);
- this.authCache.set(key, auth);
- return auth;
- }
- private invalidateAuth(account: WebDavAccount): void {
- const key = this.getAccountKey(account);
- this.authCache.delete(key);
- }
- private async login(account: WebDavAccount): Promise<JellyfinAuthContext> {
- const httpRequest = http.createHttp();
- try {
- const url = `${this.buildBaseUrl(account)}/Users/AuthenticateByName`;
- const deviceId = this.ensureDeviceId();
- const headers: JellyfinLoginHeaders = {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- 'Authorization': this.buildLoginHeader(deviceId)
- };
- const body: JellyfinAuthRequestBody = {
- Username: account.account ?? '',
- Pw: account.password ?? ''
- };
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.POST,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: headers,
- extraData: JSON.stringify(body)
- });
- if (response.responseCode < 200 || response.responseCode >= 300) {
- throw new Error(`Jellyfin 登录失败: HTTP ${response.responseCode}`);
- }
- const payload = JSON.parse(response.result as string) as JellyfinAuthResponse;
- const token = payload.AccessToken ?? '';
- const userId = payload.User?.Id ?? '';
- if (!token || !userId) {
- throw new Error('Jellyfin 登录返回缺少 AccessToken 或 UserId');
- }
- void ServerLogUtil.info(TAG, `Jellyfin 登录成功 user=${payload.User?.Name ?? ''}`);
- return {
- token,
- userId,
- deviceId,
- userName: payload.User?.Name
- };
- } finally {
- httpRequest.destroy();
- }
- }
- private buildBaseUrl(account: WebDavAccount): string {
- const protocol = account.enableHttps ? 'https' : 'http';
- const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
- const portPart = account.port && ((protocol === 'https' && account.port !== 443) || (protocol === 'http' && account.port !== 80))
- ? `:${account.port}`
- : '';
- const basePath = this.normalizeBasePath(account.jellyfinBasePath);
- return `${protocol}://${host}${portPart}${basePath}`;
- }
- private normalizeBasePath(path?: string): string {
- if (!path || path.trim().length === 0) {
- return '';
- }
- let normalized = path.trim();
- if (!normalized.startsWith('/')) {
- normalized = `/${normalized}`;
- }
- if (normalized.length > 1 && normalized.endsWith('/')) {
- normalized = normalized.slice(0, -1);
- }
- return normalized === '/' ? '' : normalized;
- }
- private buildAuthHeaderObject(auth: JellyfinAuthContext): JellyfinAuthHeaders {
- const headers: JellyfinAuthHeaders = {
- 'Authorization': this.buildAuthorizationHeader(auth),
- 'X-Emby-Token': auth.token,
- 'Accept': 'application/json'
- };
- return headers;
- }
- private buildAuthHeaderMap(auth: JellyfinAuthContext): Map<string, string> {
- const headers = new Map<string, string>();
- headers.set('Authorization', this.buildAuthorizationHeader(auth));
- headers.set('X-Emby-Token', auth.token);
- headers.set('Accept', 'application/json');
- return headers;
- }
- private buildAuthorizationHeader(auth: JellyfinAuthContext): string {
- return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${auth.deviceId}", Version="${CLIENT_VERSION}", Token="${auth.token}"`;
- }
- private buildLoginHeader(deviceId: string): string {
- return `MediaBrowser Client="${CLIENT_NAME}", Device="${DEVICE_NAME}", DeviceId="${deviceId}", Version="${CLIENT_VERSION}"`;
- }
- private ensureDeviceId(): string {
- let deviceId = PreferencesUtil.getStringSync(DEVICE_ID_KEY, '');
- if (!deviceId || deviceId.length === 0) {
- deviceId = `ttmusic-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
- PreferencesUtil.putSync(DEVICE_ID_KEY, deviceId);
- }
- return deviceId;
- }
- private buildQueryString(params?: Array<QueryParam>): string {
- if (!params) {
- return '';
- }
- const parts: string[] = [];
- for (let i = 0; i < params.length; i++) {
- const param = params[i];
- if (!param.value || param.value.length === 0) {
- continue;
- }
- parts.push(`${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`);
- }
- return parts.join('&');
- }
- private getAccountKey(account: WebDavAccount): string {
- return `${account.id ?? ''}|${account.host}|${account.port}|${account.account}`;
- }
- private toSong(item: JellyfinItem): JellyfinSong | null {
- if (!item.Id || !item.Name) {
- return null;
- }
- const mediaSource = item.MediaSources && item.MediaSources.length > 0 ? item.MediaSources[0] : undefined;
- const audioStream = mediaSource?.MediaStreams?.find(stream => stream.Type === 'Audio');
- const durationSeconds = item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined;
- const song: JellyfinSong = {
- id: item.Id,
- title: item.Name,
- album: item.Album,
- albumId: item.AlbumId,
- artist: item.Artists?.[0] ?? item.ArtistItems?.[0]?.Name,
- artistId: item.ArtistItems?.[0]?.Id,
- durationSeconds,
- size: mediaSource?.Size,
- suffix: mediaSource?.Container,
- bitRate: audioStream?.BitRate,
- sampleRate: audioStream?.SampleRate,
- track: item.IndexNumber,
- year: item.ProductionYear
- };
- return song;
- }
- }
- export const jellyfinApi = new JellyfinApi();
|