| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758 |
- import { http } from '@kit.NetworkKit';
- import { WebDavAccount } from '../../viewmodel/WebDavAccount';
- import { ServerLogUtil } from '../util/ServerLogUtil';
- import { navidromeApi } from './NavidromeApi';
- const TAG = 'heanup NavidromeRestApi';
- interface NavidromeLoginResponse {
- id?: string;
- token?: string;
- }
- interface NavidromeLoginBody {
- username: string;
- password: string;
- }
- interface NavidromeAuthContext {
- token: string;
- clientId: string;
- }
- class QueryParam {
- key: string;
- value: string;
- constructor(key: string, value: string) {
- this.key = key;
- this.value = value;
- }
- }
- export interface NavidromeRestSong {
- id: string;
- title?: string;
- album?: string;
- albumId?: string;
- artist?: string;
- artistId?: string;
- duration?: number;
- bitRate?: number;
- suffix?: string;
- size?: number;
- createdAt?: string;
- genre?: string;
- track?: number;
- year?: number;
- contentType?: string;
- coverArt?: string;
- coverArtId?: string;
- coverArtPath?: string;
- embedArtPath?: string;
- lyrics?: string; // JSON格式的歌词,需要通过 convertJsonLyricsToLrc 转换为LRC格式
- }
- export interface NavidromeRestArtist {
- id: string;
- name?: string;
- albumCount?: number;
- songCount?: number;
- playCount?: number;
- mediumImageUrl?: string;
- largeImageUrl?: string;
- coverArt?: string;
- coverArtId?: string;
- coverArtPath?: string;
- coverUrl?: string;
- }
- export interface NavidromeRestAlbum {
- id: string;
- name?: string;
- artist?: string;
- artistId?: string;
- songCount?: number;
- duration?: number;
- minYear?: number;
- maxYear?: number;
- createdAt?: string;
- embedArtPath?: string;
- coverArt?: string;
- coverArtId?: string;
- coverArtPath?: string;
- coverUrl?: string;
- }
- export interface NavidromeRestPlaylist {
- id: string;
- name?: string;
- comment?: string;
- duration?: number;
- size?: number;
- songCount?: number;
- ownerName?: string;
- ownerId?: string;
- public?: boolean;
- path?: string;
- sync?: boolean;
- createdAt?: string;
- updatedAt?: string;
- rules?: object;
- evaluatedAt?: string;
- }
- // 歌单API返回的歌曲数据,包含mediaFileId字段
- interface NavidromePlaylistSong extends NavidromeRestSong {
- mediaFileId?: string;
- }
- export interface NavidromePagedResponse<T> {
- data: T[];
- nextStart: number | null;
- }
- export class NavidromeRestApi {
- private authCache: Map<string, NavidromeAuthContext> = new Map();
- private readonly PAGE_SIZE: number = 500;
- async fetchSongPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestSong>> {
- return this.fetchPage<NavidromeRestSong>(account, '/api/song', start, 'createdAt', 'DESC');
- }
- async fetchArtistPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestArtist>> {
- return this.fetchPage<NavidromeRestArtist>(account, '/api/artist', start, 'name', 'ASC','albumartist');
- }
- async fetchAlbumPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestAlbum>> {
- return this.fetchPage<NavidromeRestAlbum>(account, '/api/album', start, 'name', 'ASC');
- }
- async fetchPlaylistPage(account: WebDavAccount, start: number): Promise<NavidromePagedResponse<NavidromeRestPlaylist>> {
- return this.fetchPage<NavidromeRestPlaylist>(account, '/api/playlist', start, 'name', 'ASC');
- }
- async fetchSongsByArtist(account: WebDavAccount, artistId: string, label?: string): Promise<NavidromeRestSong[]> {
- // 这里判断fetchSongsWithFilter如果返回空数据,则用搜索的接口去搜索艺术家 label就是艺术家名称
- const songs = await this.fetchSongsWithFilter(account, [new QueryParam('artist_id', artistId)]);
- // 如果通过 artist_id 查询结果为空,且有艺术家名称,则使用搜索接口
- if ((!songs || songs.length === 0) && label && label.trim().length > 0) {
- void ServerLogUtil.warn(TAG, `通过 artist_id 查询为空,尝试搜索艺术家: "${label}"`);
- try {
- const searchResults = await navidromeApi.searchSongs(account, label.trim(), 500);
- // 将 NavidromeSong 转换为 NavidromeRestSong
- if (searchResults && searchResults.length > 0) {
- void ServerLogUtil.info(TAG, `搜索接口返回 ${searchResults.length} 首歌曲`);
- return this.convertToRestSongs(searchResults);
- }
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `搜索艺术家歌曲失败: ${err.message}`);
- }
- }
- return songs ?? [];
- }
- async fetchSongsByAlbum(account: WebDavAccount, albumId: string, label?: string): Promise<NavidromeRestSong[]> {
- // 这里判断fetchSongsWithFilter如果返回空数据,则用搜索的接口去搜索专辑 label就是专辑名称
- const songs = await this.fetchSongsWithFilter(account, [new QueryParam('album_id', albumId)]);
- // 如果通过 album_id 查询结果为空,且有专辑名称,则使用搜索接口
- if ((!songs || songs.length === 0) && label && label.trim().length > 0) {
- void ServerLogUtil.warn(TAG, `通过 album_id 查询为空,尝试搜索专辑: "${label}"`);
- try {
- const searchResults = await navidromeApi.searchSongs(account, label.trim(), 500);
- // 将 NavidromeSong 转换为 NavidromeRestSong
- if (searchResults && searchResults.length > 0) {
- void ServerLogUtil.info(TAG, `搜索接口返回 ${searchResults.length} 首歌曲`);
- return this.convertToRestSongs(searchResults);
- }
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `搜索专辑歌曲失败: ${err.message}`);
- }
- }
- return songs ?? [];
- }
- async fetchSongsByPlaylist(account: WebDavAccount, playlistId: string): Promise<NavidromeRestSong[]> {
- const results: NavidromeRestSong[] = [];
- let start = 0;
- while (true) {
- const end = start + this.PAGE_SIZE;
- const params = [
- new QueryParam('_start', `${start}`),
- new QueryParam('_end', `${end}`),
- new QueryParam('_sort', 'createdAt'),
- new QueryParam('_order', 'DESC')
- ];
- const path = `/api/playlist/${playlistId}/tracks`;
- // 获取原始响应数据,使用NavidromePlaylistSong类型(包含mediaFileId字段)
- const chunk = await this.get<NavidromePlaylistSong[]>(account, path, params);
- if (!chunk || chunk.length === 0) {
- break;
- }
- // 处理歌单API返回的数据,将mediaFileId映射到id字段
- const processed: NavidromeRestSong[] = [];
- for (let i = 0; i < chunk.length; i++) {
- const item = chunk[i];
- // 如果存在mediaFileId,使用它作为id;否则使用原id
- const songId = item.mediaFileId && item.mediaFileId.length > 0 ? item.mediaFileId : item.id;
- if (item.mediaFileId && item.mediaFileId.length > 0) {
- void ServerLogUtil.debug('NavidromePlaylist', `歌单歌曲使用mediaFileId作为id: ${item.mediaFileId}`);
- }
- const processedItem: NavidromeRestSong = {
- id: songId,
- title: item.title,
- album: item.album,
- albumId: item.albumId,
- artist: item.artist,
- artistId: item.artistId,
- duration: item.duration,
- bitRate: item.bitRate,
- suffix: item.suffix,
- size: item.size,
- createdAt: item.createdAt,
- genre: item.genre,
- track: item.track,
- year: item.year,
- contentType: item.contentType,
- coverArt: item.coverArt,
- coverArtId: item.coverArtId,
- coverArtPath: item.coverArtPath,
- embedArtPath: item.embedArtPath,
- lyrics: item.lyrics,
- };
- processed.push(processedItem);
- }
- results.push(...processed);
- if (processed.length < this.PAGE_SIZE) {
- break;
- }
- start = end;
- }
- return results;
- }
- // 将 NavidromeSong 转换为 NavidromeRestSong
- private convertToRestSongs(songs: import('./NavidromeApi').NavidromeSong[]): NavidromeRestSong[] {
- return songs.map((song): NavidromeRestSong => ({
- id: song.id,
- title: song.title,
- artist: song.artist,
- artistId: song.artistId,
- album: song.album,
- albumId: song.albumId,
- duration: song.duration,
- bitRate: song.bitRate,
- suffix: song.suffix,
- size: song.size,
- createdAt: song.created,
- genre: song.genre,
- track: song.track,
- year: song.year,
- contentType: song.contentType,
- coverArt: song.coverArt,
- coverArtId: song.coverArt,
- coverArtPath: undefined,
- embedArtPath: undefined,
- lyrics: song.lyrics,
- }));
- }
- private async fetchPage<T extends object>(account: WebDavAccount, path: string,
- start: number, sortField: string, order: 'ASC' | 'DESC',role?:string): Promise<NavidromePagedResponse<T>> {
- const end = start + this.PAGE_SIZE;
- const params: Array<QueryParam> = [
- new QueryParam('_start', `${start}`),
- new QueryParam('_end', `${end}`),
- new QueryParam('_sort', sortField),
- new QueryParam('_order', order)
- ];
- if(role){
- params.push(new QueryParam('_role', role));
- }
- void ServerLogUtil.debug(TAG, `${path} 分页请求: start=${start}, end=${end}`);
- const chunk = await this.get<T[]>(account, path, params);
- const nextStart = chunk && chunk.length === this.PAGE_SIZE ? end : null;
- return {
- data: chunk ?? [],
- nextStart
- };
- }
- private async fetchSongsWithFilter(account: WebDavAccount, extraParams: QueryParam[]): Promise<NavidromeRestSong[]> {
- const results: NavidromeRestSong[] = [];
- let start = 0;
- while (true) {
- const end = start + this.PAGE_SIZE;
- const params = [
- new QueryParam('_start', `${start}`),
- new QueryParam('_end', `${end}`),
- new QueryParam('_sort', 'createdAt'),
- new QueryParam('_order', 'DESC'),
- ...extraParams
- ];
- const chunk = await this.get<NavidromeRestSong[]>(account, '/api/song', params);
- if (!chunk || chunk.length === 0) {
- break;
- }
- results.push(...chunk);
- if (chunk.length < this.PAGE_SIZE) {
- break;
- }
- start = end;
- }
- return results;
- }
- private async get<T>(account: WebDavAccount, path: string, params?: Array<QueryParam>, retry: boolean = true): Promise<T> {
- const httpRequest = http.createHttp();
- try {
- const auth = await this.ensureAuth(account);
- const query = this.buildQueryString(params);
- const url = `${this.buildRootBase(account)}${path}${query}`;
- void ServerLogUtil.info(TAG, `GET ${url}`);
- void ServerLogUtil.debug(TAG, `请求参数: ${JSON.stringify(params ?? [])}`)
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.GET,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: this.buildAuthHeader(auth)
- });
- if (response.responseCode === 401 && retry) {
- this.invalidateAuth(account);
- void ServerLogUtil.warn(TAG, `401需要重试: ${url}`)
- return this.get(account, path, params, false);
- }
- if (response.responseCode !== 200) {
- void ServerLogUtil.error(TAG, `GET ${url} 失败 code=${response.responseCode}`);
- throw new Error(`Navidrome API 请求失败: HTTP ${response.responseCode}`);
- }
- void ServerLogUtil.info(TAG, `GET ${url} 成功 code=${response.responseCode}`);
- return JSON.parse(response.result as string) as T;
- } finally {
- httpRequest.destroy();
- }
- }
- private async ensureAuth(account: WebDavAccount): Promise<NavidromeAuthContext> {
- 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);
- if (this.authCache.has(key)) {
- this.authCache.delete(key);
- }
- }
- private async login(account: WebDavAccount): Promise<NavidromeAuthContext> {
- const httpRequest = http.createHttp();
- try {
- const rootBase = this.buildRootBase(account);
- const url = `${rootBase}/auth/login`;
- const username = account.account?.trim();
- const password = account.password?.trim();
- if (!username || !password) {
- throw new Error('Navidrome账号缺少用户名或密码');
- }
- void ServerLogUtil.info(TAG, `登录 ${ServerLogUtil.sanitizeAccount(account)}`)
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.POST,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: {
- 'Content-Type': 'application/json; charset=utf-8',
- 'Accept': 'application/json; charset=utf-8',
- 'Accept-Charset': 'utf-8'
- },
- extraData: JSON.stringify(this.buildLoginBody(username, password))
- });
- if (response.responseCode !== 200) {
- void ServerLogUtil.error(TAG, `登录失败 code=${response.responseCode}`);
- throw new Error(`Navidrome 登录失败: HTTP ${response.responseCode}`);
- }
- const body = JSON.parse(response.result as string) as NavidromeLoginResponse;
- if (!body.token || !body.id) {
- throw new Error('Navidrome 登录响应缺少 token 信息');
- }
- void ServerLogUtil.info(TAG, '登录成功,已获取token');
- return {
- token: body.token,
- clientId: body.id
- };
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `登录异常: ${err.message}`);
- throw err;
- } finally {
- httpRequest.destroy();
- }
- }
- private buildAuthHeader(auth: NavidromeAuthContext): Record<string, string> {
- return {
- 'x-nd-authorization': `Bearer ${auth.token}`,
- 'x-nd-client-unique-id': auth.clientId,
- 'Accept': 'application/json; charset=utf-8',
- 'Accept-Charset': 'utf-8'
- };
- }
- private buildLoginBody(username: string, password: string): NavidromeLoginBody {
- const body: NavidromeLoginBody = {
- username,
- password
- };
- return body;
- }
- private buildQueryString(params?: Array<QueryParam>): string {
- if (!params || params.length === 0) {
- return '';
- }
- const parts: string[] = [];
- for (let i = 0; i < params.length; i++) {
- const param = params[i];
- parts.push(`${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`);
- }
- return parts.length > 0 ? `?${parts.join('&')}` : '';
- }
- private buildRootBase(account: WebDavAccount): string {
- const protocol = account.enableHttps ? 'https' : 'http';
- const host = (account.isUseLocalHost && account.localHost ? account.localHost : account.host)?.trim();
- if (!host || host.length === 0) {
- throw new Error('Navidrome账号缺少服务器地址');
- }
- const port = account.port && account.port > 0 ? `:${account.port}` : '';
- const prefix = this.resolveRootPath(account.navidromeBasePath);
- void ServerLogUtil.debug(TAG, `buildRootBase => ${protocol}://${host}${port}${prefix}`)
- return `${protocol}://${host}${port}${prefix}`;
- }
- private resolveRootPath(path?: string): string {
- if (!path) {
- return '';
- }
- let normalized = path.trim();
- if (normalized.length === 0) {
- return '';
- }
- if (!normalized.startsWith('/')) {
- normalized = `/${normalized}`;
- }
- while (normalized.endsWith('/') && normalized.length > 1) {
- normalized = normalized.slice(0, -1);
- }
- const lower = normalized.toLowerCase();
- if (lower === '/rest') {
- return '';
- }
- if (lower.endsWith('/rest')) {
- const prefix = normalized.slice(0, normalized.length - 5);
- return prefix === '/' ? '' : prefix;
- }
- return normalized === '/' ? '' : normalized;
- }
- /**
- * 获取歌曲详细信息(用于调试)
- * 使用 Navidrome REST API: GET /api/song/{id}
- * @param account Navidrome 账号信息
- * @param songId 歌曲ID
- * @returns 歌曲详细信息对象
- */
- async getLyricsBySongId(account: WebDavAccount, songId: string): Promise<string | undefined> {
- try {
- void ServerLogUtil.info(TAG, `========== 获取歌曲详细信息 ==========`)
- void ServerLogUtil.info(TAG, `歌曲ID: ${songId}`)
- const path = `/api/song/${songId}`;
- const song = await this.get<NavidromeRestSong>(account, path);
- if (song) {
- void ServerLogUtil.info(TAG, `✅ 获取歌曲信息成功`);
- void ServerLogUtil.info(TAG, `完整JSON:\n${JSON.stringify(song, null, 2)}`);
- // 特别检查歌词字段
- if (song.lyrics) {
- void ServerLogUtil.info(TAG, `✅ 歌曲包含歌词字段`);
- void ServerLogUtil.info(TAG, `原始歌词JSON:\n${song.lyrics}`);
- // 将JSON格式的歌词转换为LRC格式
- const lrcLyrics = this.convertJsonLyricsToLrc(song.lyrics);
- void ServerLogUtil.info(TAG, `✅ 转换后的LRC歌词:\n${lrcLyrics}`);
- return lrcLyrics;
- } else {
- void ServerLogUtil.warn(TAG, `⚠️ 歌曲没有歌词字段`);
- }
- } else {
- void ServerLogUtil.error(TAG, `❌ 获取歌曲信息失败,返回为空`);
- }
- return undefined;
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `❌ 获取歌曲详细信息异常: ${err.message}`);
- return undefined;
- }
- }
- /**
- * 将Navidrome的JSON格式歌词转换为LRC格式
- * @param jsonLyrics JSON格式的歌词字符串
- * @returns LRC格式的歌词字符串
- */
- private convertJsonLyricsToLrc(jsonLyrics: string): string {
- try {
- void ServerLogUtil.debug(TAG, `开始转换JSON歌词到LRC格式`);
- // 解析JSON,使用明确的类型
- const jsonData: Array<object> | null = JSON.parse(jsonLyrics) as Array<object> | null;
- // 检查是否是数组格式
- if (!jsonData || jsonData.length === 0) {
- void ServerLogUtil.warn(TAG, `歌词不是数组格式或为空,直接返回原文本`);
- return jsonLyrics;
- }
- const lrcLines: string[] = [];
- // 遍历所有语言版本
- for (let i = 0; i < jsonData.length; i++) {
- const langItem = jsonData[i];
- if (!langItem) {
- continue;
- }
- // 定义歌词行接口
- interface LyricLine {
- start: number;
- value: string;
- }
- // 定义语言数据接口
- interface LangData {
- lang: string;
- line: LyricLine[];
- }
- // 使用接口类型进行类型检查
- if (this.isValidLangData(langItem)) {
- const langData: LangData = langItem as LangData;
- void ServerLogUtil.debug(TAG, `处理语言: ${langData.lang}, 歌词行数: ${langData.line.length}`);
- // 将每一行转换为LRC格式
- for (let j = 0; j < langData.line.length; j++) {
- const lineData = langData.line[j];
- if (lineData && typeof lineData.start === 'number' && typeof lineData.value === 'string') {
- const start = lineData.start;
- const value = lineData.value;
- // 转换时间为LRC格式 [mm:ss.ms]
- const minutes = Math.floor(start / 60000);
- const seconds = Math.floor((start % 60000) / 1000);
- const milliseconds = start % 1000;
- const timeTag = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}]`;
- lrcLines.push(`${timeTag}${value}`);
- }
- }
- }
- }
- // 按时间排序
- lrcLines.sort();
- const result = lrcLines.join('\n');
- void ServerLogUtil.info(TAG, `✅ 成功转换歌词,共 ${lrcLines.length} 行`);
- return result;
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `❌ 转换歌词格式失败: ${err.message}`);
- void ServerLogUtil.debug(TAG, `返回原始歌词文本`);
- return jsonLyrics;
- }
- }
- /**
- * 检查对象是否是有效的语言数据结构
- */
- private isValidLangData(obj: object): boolean {
- if (!obj || typeof obj !== 'object') {
- return false;
- }
- const record = obj as Record<string, object>;
- // 检查是否有 lang 和 line 属性
- if (!record.lang || !record.line) {
- return false;
- }
- // 检查 lang 是否是字符串
- if (typeof record.lang !== 'string') {
- return false;
- }
- // 检查 line 是否是数组
- if (!Array.isArray(record.line)) {
- return false;
- }
- return true;
- }
- /**
- * 获取歌词 (旧方式 - 通过 artist 和 title)
- * 使用 Subsonic API: /rest/getLyrics
- * @param account Navidrome 账号信息
- * @param artist 歌手名(可选)
- * @param title 歌曲名(可选)
- * @returns 歌词文本,如果获取失败返回空字符串返回的歌词没有时间戳,所以废弃使用
- * @deprecated 建议使用 getLyricsBySongId 代替
- */
- async getLyrics(account: WebDavAccount, artist?: string, title?: string): Promise<string> {
- const httpRequest = http.createHttp();
- try {
- // 构建Subsonic API参数
- const params: Array<QueryParam> = [
- new QueryParam('u', account.account ?? ''),
- new QueryParam('p', account.password ?? ''),
- new QueryParam('v', '1.16.1'),
- new QueryParam('c', 'TTMusic'),
- new QueryParam('f', 'json')
- ];
- // 添加可选参数
- if (artist && artist.trim().length > 0) {
- params.push(new QueryParam('artist', artist.trim()));
- }
- if (title && title.trim().length > 0) {
- params.push(new QueryParam('title', title.trim()));
- }
- const query = this.buildQueryString(params);
- const url = `${this.buildRootBase(account)}/rest/getLyrics${query}`;
- void ServerLogUtil.info(TAG, `获取歌词 GET ${url}`);
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.GET,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING,
- header: {
- 'Accept': 'application/xml; charset=utf-8',
- 'Accept-Charset': 'utf-8'
- }
- });
- if (response.responseCode !== 200) {
- void ServerLogUtil.error(TAG, `获取歌词失败 code=${response.responseCode}`);
- return '';
- }
- void ServerLogUtil.info(TAG, `获取歌词成功 code=${response.responseCode}`);
- const xmlText = response.result as string;
- void ServerLogUtil.info(TAG, `获取歌词成功 xmlText=${xmlText}`);
- // 解析XML响应,提取lyrics标签内容
- return this.parseLyricsFromXml(xmlText);
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `获取歌词异常: ${err.message}`);
- return '';
- } finally {
- httpRequest.destroy();
- }
- }
- /**
- * 从Subsonic API的XML响应中解析歌词文本
- * @param xmlText XML响应文本
- * @returns 歌词文本,如果解析失败返回空字符串
- */
- private parseLyricsFromXml(xmlText: string): string {
- try {
- // 查找 <lyrics> 标签
- const lyricsMatch = xmlText.match(/<lyrics[^>]*>([\s\S]*?)<\/lyrics>/);
- if (!lyricsMatch || lyricsMatch.length < 2) {
- void ServerLogUtil.warn(TAG, 'XML响应中未找到lyrics标签');
- return '';
- }
- // 提取歌词内容并处理XML转义字符
- let lyrics = lyricsMatch[1];
- // 处理XML转义字符
- lyrics = lyrics
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, "'")
- .trim();
- void ServerLogUtil.info(TAG, `成功解析歌词 lyrics=${lyrics}`);
- return lyrics;
- } catch (error) {
- const err = error as Error;
- void ServerLogUtil.error(TAG, `解析歌词XML失败: ${err.message}`);
- return '';
- }
- }
- resolveResourceUrl(account: WebDavAccount, path?: string): string | undefined {
- if (!path || path.trim().length === 0) {
- return undefined;
- }
- let normalized = path.trim();
- if (normalized.startsWith('http')) {
- return normalized;
- }
- if (!normalized.startsWith('/')) {
- normalized = `/${normalized}`;
- }
- return `${this.buildRootBase(account)}${normalized}`;
- }
- private getAccountKey(account: WebDavAccount): string {
- if (account.id && account.id > 0) {
- return account.id.toString();
- }
- return `${account.host ?? ''}_${account.account ?? ''}`;
- }
- }
- export const navidromeRestApi = new NavidromeRestApi();
|