|
|
@@ -0,0 +1,513 @@
|
|
|
+import { http } from '@kit.NetworkKit';
|
|
|
+import { PreferencesUtil } from '@pura/harmony-utils';
|
|
|
+import { WebDavAccount } from '../../viewmodel/WebDavAccount';
|
|
|
+import { ServerLogUtil } from '../util/ServerLogUtil';
|
|
|
+
|
|
|
+const TAG = 'heanup EmbyApi';
|
|
|
+const CLIENT_NAME = 'TTMusic';
|
|
|
+const CLIENT_VERSION = '1.0.0';
|
|
|
+const DEVICE_NAME = 'HarmonyOS';
|
|
|
+const DEVICE_ID_KEY = 'emby_device_id';
|
|
|
+
|
|
|
+interface EmbyAuthResponse {
|
|
|
+ AccessToken?: string;
|
|
|
+ User?: EmbyUser;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyUser {
|
|
|
+ Id?: string;
|
|
|
+ Name?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyAuthContext {
|
|
|
+ token: string;
|
|
|
+ userId: string;
|
|
|
+ deviceId: string;
|
|
|
+ userName?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyItemImageTags {
|
|
|
+ Primary?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyItem {
|
|
|
+ Id?: string;
|
|
|
+ Name?: string;
|
|
|
+ Type?: string;
|
|
|
+ Album?: string;
|
|
|
+ AlbumId?: string;
|
|
|
+ Artists?: string[];
|
|
|
+ ArtistItems?: Array<EmbyPerson>;
|
|
|
+ AlbumArtists?: Array<EmbyPerson>;
|
|
|
+ RunTimeTicks?: number;
|
|
|
+ ProductionYear?: number;
|
|
|
+ IndexNumber?: number;
|
|
|
+ ImageTags?: EmbyItemImageTags;
|
|
|
+ MediaSources?: Array<EmbyMediaSource>;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyItemsResponse {
|
|
|
+ Items?: EmbyItem[];
|
|
|
+ TotalRecordCount?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyPerson {
|
|
|
+ Id?: string;
|
|
|
+ Name?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyMediaStream {
|
|
|
+ Type?: string;
|
|
|
+ Codec?: string;
|
|
|
+ Index?: number;
|
|
|
+ BitRate?: number;
|
|
|
+ SampleRate?: number;
|
|
|
+ Channels?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyMediaSource {
|
|
|
+ Id?: string;
|
|
|
+ MediaStreams?: EmbyMediaStream[];
|
|
|
+ Size?: number;
|
|
|
+ Container?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyLyricTrackEvent {
|
|
|
+ Text?: string;
|
|
|
+ StartPositionTicks?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyLyricData {
|
|
|
+ TrackEvents?: EmbyLyricTrackEvent[];
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyAuthRequestBody {
|
|
|
+ Username: string;
|
|
|
+ Pw: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyAuthHeaders {
|
|
|
+ Authorization: string;
|
|
|
+ 'X-Emby-Token': string;
|
|
|
+ Accept: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface EmbyLoginHeaders {
|
|
|
+ '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 EmbyArtist {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ albumCount?: number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface EmbyAlbum {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ artist?: string;
|
|
|
+ songCount?: number;
|
|
|
+ year?: number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface EmbySong {
|
|
|
+ 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;
|
|
|
+ lyricIndex?: number; // 歌词流索引
|
|
|
+}
|
|
|
+
|
|
|
+export class EmbyApi {
|
|
|
+ private authCache: Map<string, EmbyAuthContext> = new Map();
|
|
|
+
|
|
|
+ async getArtists(account: WebDavAccount): Promise<EmbyArtist[]> {
|
|
|
+ const auth = await this.ensureAuth(account);
|
|
|
+ const params: Array<QueryParam> = [
|
|
|
+ new QueryParam('SortBy', 'SortName'),
|
|
|
+ new QueryParam('SortOrder', 'Ascending')
|
|
|
+ ];
|
|
|
+ const response = await this.get<EmbyItemsResponse>(account, '/Artists', params);
|
|
|
+ const items = response.Items ?? [];
|
|
|
+ return items
|
|
|
+ .filter(item => item.Id && item.Name)
|
|
|
+ .map(item => {
|
|
|
+ const artist: EmbyArtist = {
|
|
|
+ id: item.Id as string,
|
|
|
+ name: item.Name as string
|
|
|
+ };
|
|
|
+ return artist;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async getAlbumArtists(account: WebDavAccount): Promise<EmbyArtist[]> {
|
|
|
+ const params: Array<QueryParam> = [
|
|
|
+ new QueryParam('SortBy', 'SortName'),
|
|
|
+ new QueryParam('SortOrder', 'Ascending')
|
|
|
+ ];
|
|
|
+ const response = await this.get<EmbyItemsResponse>(account, '/Artists/AlbumArtists', params);
|
|
|
+ const items = response.Items ?? [];
|
|
|
+ return items
|
|
|
+ .filter(item => item.Id && item.Name)
|
|
|
+ .map(item => {
|
|
|
+ const artist: EmbyArtist = {
|
|
|
+ id: item.Id as string,
|
|
|
+ name: item.Name as string
|
|
|
+ };
|
|
|
+ return artist;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async getArtistAlbums(account: WebDavAccount, artistId: string): Promise<EmbyAlbum[]> {
|
|
|
+ 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<EmbyItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
|
|
|
+ const items = response.Items ?? [];
|
|
|
+ return items
|
|
|
+ .filter(item => item.Id && item.Name)
|
|
|
+ .map(item => {
|
|
|
+ const album: EmbyAlbum = {
|
|
|
+ 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<EmbyAlbum | null> {
|
|
|
+ const auth = await this.ensureAuth(account);
|
|
|
+ const item = await this.get<EmbyItem>(account, `/Users/${auth.userId}/Items/${albumId}`);
|
|
|
+ if (!item || !item.Id || !item.Name) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const album: EmbyAlbum = {
|
|
|
+ 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<EmbySong[]> {
|
|
|
+ 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<EmbyItemsResponse>(account, `/Users/${auth.userId}/Items`, params);
|
|
|
+ const items = response.Items ?? [];
|
|
|
+ const songs: EmbySong[] = [];
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const song = this.toSong(items[i]);
|
|
|
+ if (song) {
|
|
|
+ songs.push(song);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return songs;
|
|
|
+ }
|
|
|
+
|
|
|
+ async buildStreamUrl(account: WebDavAccount, itemId: string): Promise<string> {
|
|
|
+ const baseUrl = this.buildBaseUrl(account);
|
|
|
+ return `${baseUrl}/Audio/${encodeURIComponent(itemId)}/stream?static=true`;
|
|
|
+ }
|
|
|
+
|
|
|
+ async buildPrimaryImageUrl(account: WebDavAccount, itemId: string, width: number = 600, height: number = 600): Promise<string> {
|
|
|
+ const baseUrl = this.buildBaseUrl(account);
|
|
|
+ return `${baseUrl}/Items/${encodeURIComponent(itemId)}/Images/Primary?fillHeight=${height}&fillWidth=${width}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ async getLyric(account: WebDavAccount, itemId: string, lyricIndex: number): Promise<string> {
|
|
|
+ const auth = await this.ensureAuth(account);
|
|
|
+ const baseUrl = this.buildBaseUrl(account);
|
|
|
+ const mediaSourceId = itemId; // 通常 MediaSourceId 与 ItemId 相同
|
|
|
+ const url = `${baseUrl}/Items/${encodeURIComponent(itemId)}/${encodeURIComponent(mediaSourceId)}/Subtitles/${lyricIndex}/Stream.js`;
|
|
|
+ const httpRequest = http.createHttp();
|
|
|
+ try {
|
|
|
+ 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) {
|
|
|
+ throw new Error(`Emby 获取歌词失败: HTTP ${response.responseCode}`);
|
|
|
+ }
|
|
|
+ const lyricData = JSON.parse(response.result as string) as EmbyLyricData;
|
|
|
+ // 转换为标准 LRC 格式
|
|
|
+ return this.convertEmbyLyricToLrc(lyricData);
|
|
|
+ } finally {
|
|
|
+ httpRequest.destroy();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getAuthHeaders(account: WebDavAccount): Promise<Map<string, string>> {
|
|
|
+ const auth = await this.ensureAuth(account);
|
|
|
+ return this.buildAuthHeaderMap(auth);
|
|
|
+ }
|
|
|
+
|
|
|
+ private convertEmbyLyricToLrc(lyricData: EmbyLyricData): string {
|
|
|
+ if (!lyricData || !lyricData.TrackEvents || !Array.isArray(lyricData.TrackEvents)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ const lines: string[] = [];
|
|
|
+ lyricData.TrackEvents.forEach((event: EmbyLyricTrackEvent) => {
|
|
|
+ if (event.Text && event.StartPositionTicks !== undefined) {
|
|
|
+ // Ticks 转换为秒(1 tick = 100ns = 0.0000001s)
|
|
|
+ const seconds = event.StartPositionTicks / 10000000;
|
|
|
+ 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}${event.Text}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return lines.join('\n');
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 post<T>(account: WebDavAccount, path: string, body?: object): Promise<T> {
|
|
|
+ return this.request<T>(account, http.RequestMethod.POST, path, undefined, body);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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(`Emby API 请求失败: HTTP ${response.responseCode}`);
|
|
|
+ }
|
|
|
+ return JSON.parse(response.result as string) as T;
|
|
|
+ } finally {
|
|
|
+ httpRequest.destroy();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async ensureAuth(account: WebDavAccount): Promise<EmbyAuthContext> {
|
|
|
+ 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<EmbyAuthContext> {
|
|
|
+ const httpRequest = http.createHttp();
|
|
|
+ try {
|
|
|
+ const url = `${this.buildBaseUrl(account)}/Users/AuthenticateByName`;
|
|
|
+ const deviceId = this.ensureDeviceId();
|
|
|
+ const headers: EmbyLoginHeaders = {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Accept': 'application/json',
|
|
|
+ 'Authorization': this.buildLoginHeader(deviceId)
|
|
|
+ };
|
|
|
+ const body: EmbyAuthRequestBody = {
|
|
|
+ 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(`Emby 登录失败: HTTP ${response.responseCode}`);
|
|
|
+ }
|
|
|
+ const payload = JSON.parse(response.result as string) as EmbyAuthResponse;
|
|
|
+ const token = payload.AccessToken ?? '';
|
|
|
+ const userId = payload.User?.Id ?? '';
|
|
|
+ if (!token || !userId) {
|
|
|
+ throw new Error('Emby 登录返回缺少 AccessToken 或 UserId');
|
|
|
+ }
|
|
|
+ void ServerLogUtil.info(TAG, `Emby 登录成功 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.embyBasePath);
|
|
|
+ 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: EmbyAuthContext): EmbyAuthHeaders {
|
|
|
+ const headers: EmbyAuthHeaders = {
|
|
|
+ 'Authorization': this.buildAuthorizationHeader(auth),
|
|
|
+ 'X-Emby-Token': auth.token,
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ };
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildAuthHeaderMap(auth: EmbyAuthContext): 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: EmbyAuthContext): 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: EmbyItem): EmbySong | null {
|
|
|
+ if (!item.Id || !item.Name) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const mediaSource: EmbyMediaSource | undefined = item.MediaSources && item.MediaSources.length > 0
|
|
|
+ ? item.MediaSources[0]
|
|
|
+ : undefined;
|
|
|
+ const audioStream: EmbyMediaStream | undefined = mediaSource?.MediaStreams?.find((stream: EmbyMediaStream) => stream.Type === 'Audio');
|
|
|
+ // 查找歌词流索引
|
|
|
+ const lyricStream: EmbyMediaStream | undefined = mediaSource?.MediaStreams?.find((stream: EmbyMediaStream) =>
|
|
|
+ stream.Type === 'Subtitle' && stream.Codec === 'lrc'
|
|
|
+ );
|
|
|
+ const durationSeconds = item.RunTimeTicks ? Math.floor(item.RunTimeTicks / 10000000) : undefined;
|
|
|
+ const song: EmbySong = {
|
|
|
+ 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,
|
|
|
+ lyricIndex: lyricStream?.Index
|
|
|
+ };
|
|
|
+ return song;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const embyApi = new EmbyApi();
|