| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911 |
- import { http } from '@kit.NetworkKit';
- import { PreferencesUtil } from '@pura/harmony-utils';
- import { WebDavAccount } from '../../viewmodel/WebDavAccount';
- import { ServerLogUtil } from '../util/ServerLogUtil';
- const TAG = 'heanup AudioStationApi';
- const DEVICE_ID_KEY = 'audiostation_device_id';
- const DEVICE_NAME = 'HarmonyOS';
- interface AudioStationAuthContext {
- sid: string;
- did?: string;
- }
- interface AudioStationError {
- code?: number;
- errors?: Record<string, string>;
- }
- interface AudioStationResponse<T> {
- success: boolean;
- data?: T;
- error?: AudioStationError;
- }
- interface AudioStationSongAudio {
- bitrate?: number;
- channel?: number;
- codec?: string;
- container?: string;
- duration?: number;
- filesize?: number;
- frequency?: number;
- }
- interface AudioStationSongTag {
- album?: string;
- album_artist?: string;
- artist?: string;
- genre?: string;
- track?: number;
- year?: number;
- }
- interface AudioStationSongEntry {
- id?: string;
- title?: string;
- path?: string;
- additional?: AudioStationSongAdditional;
- }
- interface AudioStationSongAdditional {
- song_audio?: AudioStationSongAudio;
- song_tag?: AudioStationSongTag;
- }
- interface AudioStationSongListData {
- songs?: AudioStationSongEntry[];
- offset?: number;
- total?: number;
- }
- interface AudioStationArtistEntry {
- name?: string;
- }
- interface AudioStationArtistListData {
- artists?: AudioStationArtistEntry[];
- offset?: number;
- total?: number;
- }
- interface AudioStationAlbumEntry {
- name?: string;
- album_artist?: string;
- display_artist?: string;
- year?: number;
- }
- interface AudioStationAlbumListData {
- albums?: AudioStationAlbumEntry[];
- offset?: number;
- total?: number;
- }
- interface AudioStationPlaylistEntry {
- id?: string;
- name?: string;
- type?: string;
- library?: string;
- sharing_status?: string;
- path?: string;
- }
- interface AudioStationPlaylistListData {
- playlists?: AudioStationPlaylistEntry[];
- offset?: number;
- total?: number;
- }
- interface AudioStationPlaylistSongEntry {
- id?: string;
- title?: string;
- path?: string;
- additional?: AudioStationSongAdditional;
- }
- interface AudioStationPlaylistInfoData {
- songs?: AudioStationPlaylistSongEntry[];
- offset?: number;
- total?: number;
- }
- interface AudioStationSearchData {
- songs?: AudioStationSongEntry[];
- songTotal?: number;
- artists?: AudioStationArtistEntry[];
- artistTotal?: number;
- albums?: AudioStationAlbumEntry[];
- albumTotal?: number;
- }
- interface AudioStationPagedResponse<T> {
- items: T[];
- nextStart: number | null;
- }
- interface AudioStationLoginRequestBody {
- api: string;
- version: string;
- method: string;
- session: string;
- account: string;
- passwd: string;
- enable_device_token: string;
- device_name: string;
- device_id: string;
- }
- interface AudioStationLoginResponseData {
- sid?: string;
- did?: string;
- }
- type AudioStationRequestBody = AudioStationLoginRequestBody;
- type JsonValue = string | number | boolean | null | Object | Array<JsonValue>;
- class QueryParam {
- key: string;
- value: string;
- constructor(key: string, value: string) {
- this.key = key;
- this.value = value;
- }
- }
- export interface AudioStationArtist {
- name: string;
- }
- export interface AudioStationAlbum {
- name: string;
- albumArtist?: string;
- displayArtist?: string;
- year?: number;
- }
- export interface AudioStationSong {
- id: string;
- title?: string;
- path?: string;
- album?: string;
- albumArtist?: string;
- artist?: string;
- durationSeconds?: number;
- size?: number;
- bitRate?: number;
- sampleRate?: number;
- codec?: string;
- container?: string;
- track?: number;
- year?: number;
- }
- export interface AudioStationPlaylist {
- id: string;
- name: string;
- type?: string;
- library?: string;
- path?: string;
- }
- export class AudioStationApi {
- private authCache: Map<string, AudioStationAuthContext> = new Map();
- async getArtists(account: WebDavAccount, offset: number = 0, limit: number = 200): Promise<AudioStationArtist[]> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/artist.cgi');
- const artists: AudioStationArtist[] = [];
- let currentOffset = offset;
- while (true) {
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Artist'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${currentOffset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'name'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.get<AudioStationResponse<AudioStationArtistListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取艺术家失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.artists ?? [];
- const mapped = chunk
- .filter(item => item.name)
- .map(item => {
- const artist: AudioStationArtist = { name: item.name as string };
- return artist;
- });
- artists.push(...mapped);
- const total = response.data?.total ?? artists.length;
- if (currentOffset + chunk.length >= total || chunk.length === 0) {
- break;
- }
- currentOffset += chunk.length;
- }
- return artists;
- }
- async getArtistsPage(account: WebDavAccount, offset: number = 0, limit: number = 200): Promise<AudioStationPagedResponse<AudioStationArtist>> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/artist.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Artist'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'name'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.get<AudioStationResponse<AudioStationArtistListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取艺术家失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.artists ?? [];
- const items = chunk
- .filter(item => item.name)
- .map(item => {
- const artist: AudioStationArtist = { name: item.name as string };
- return artist;
- });
- const total = response.data?.total ?? items.length;
- const nextStart = offset + items.length < total ? offset + items.length : null;
- const result: AudioStationPagedResponse<AudioStationArtist> = {
- items,
- nextStart
- };
- return result;
- }
- async getAlbums(account: WebDavAccount, artistName?: string, offset: number = 0, limit: number = 200): Promise<AudioStationAlbum[]> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/album.cgi');
- const albums: AudioStationAlbum[] = [];
- let currentOffset = offset;
- while (true) {
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Album'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${currentOffset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'name'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('_sid', auth.sid)
- ];
- if (artistName) {
- params.push(new QueryParam('artist', artistName));
- }
- const response = await this.get<AudioStationResponse<AudioStationAlbumListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取专辑失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.albums ?? [];
- const mapped = chunk
- .filter(item => item.name)
- .map(item => {
- const album: AudioStationAlbum = {
- name: item.name as string,
- albumArtist: item.album_artist,
- displayArtist: item.display_artist,
- year: item.year
- };
- return album;
- });
- albums.push(...mapped);
- const total = response.data?.total ?? albums.length;
- if (currentOffset + chunk.length >= total || chunk.length === 0) {
- break;
- }
- currentOffset += chunk.length;
- }
- return albums;
- }
- async getAlbumsPage(
- account: WebDavAccount,
- artistName?: string,
- offset: number = 0,
- limit: number = 200
- ): Promise<AudioStationPagedResponse<AudioStationAlbum>> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/album.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Album'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'name'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('_sid', auth.sid)
- ];
- if (artistName) {
- params.push(new QueryParam('artist', artistName));
- }
- const response = await this.get<AudioStationResponse<AudioStationAlbumListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取专辑失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.albums ?? [];
- const items = chunk
- .filter(item => item.name)
- .map(item => {
- const album: AudioStationAlbum = {
- name: item.name as string,
- albumArtist: item.album_artist,
- displayArtist: item.display_artist,
- year: item.year
- };
- return album;
- });
- const total = response.data?.total ?? items.length;
- const nextStart = offset + items.length < total ? offset + items.length : null;
- const result: AudioStationPagedResponse<AudioStationAlbum> = {
- items,
- nextStart
- };
- return result;
- }
- async getAlbumSongs(
- account: WebDavAccount,
- albumName: string,
- albumArtist?: string,
- offset: number = 0,
- limit: number = 500
- ): Promise<AudioStationSong[]> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/song.cgi');
- const songs: AudioStationSong[] = [];
- let currentOffset = offset;
- while (true) {
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Song'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${currentOffset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'title'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('additional', 'song_tag,song_audio'),
- new QueryParam('_sid', auth.sid),
- new QueryParam('album', albumName)
- ];
- if (albumArtist) {
- params.push(new QueryParam('album_artist', albumArtist));
- }
- const response = await this.postForm<AudioStationResponse<AudioStationSongListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取歌曲失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.songs ?? [];
- songs.push(...chunk
- .filter(item => item.id)
- .map(item => {
- const audio = item.additional?.song_audio;
- const tag = item.additional?.song_tag;
- const duration = audio?.duration;
- const song: AudioStationSong = {
- id: item.id as string,
- title: item.title,
- path: item.path,
- album: tag?.album,
- albumArtist: tag?.album_artist,
- artist: tag?.artist,
- durationSeconds: duration ? Math.round(duration) : undefined,
- size: audio?.filesize,
- bitRate: audio?.bitrate,
- sampleRate: audio?.frequency,
- codec: audio?.codec,
- container: audio?.container,
- track: tag?.track,
- year: tag?.year
- };
- return song;
- }));
- const total = response.data?.total ?? songs.length;
- if (currentOffset + chunk.length >= total || chunk.length === 0) {
- break;
- }
- currentOffset += chunk.length;
- }
- return songs;
- }
- async getSongs(account: WebDavAccount, offset: number = 0, limit: number = 500): Promise<AudioStationSong[]> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/song.cgi');
- const songs: AudioStationSong[] = [];
- let currentOffset = offset;
- while (true) {
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Song'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${currentOffset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'title'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('additional', 'song_tag,song_audio'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.postForm<AudioStationResponse<AudioStationSongListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取歌曲失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.songs ?? [];
- songs.push(...chunk
- .filter(item => item.id)
- .map(item => {
- const audio = item.additional?.song_audio;
- const tag = item.additional?.song_tag;
- const duration = audio?.duration;
- const song: AudioStationSong = {
- id: item.id as string,
- title: item.title,
- path: item.path,
- album: tag?.album,
- albumArtist: tag?.album_artist,
- artist: tag?.artist,
- durationSeconds: duration ? Math.round(duration) : undefined,
- size: audio?.filesize,
- bitRate: audio?.bitrate,
- sampleRate: audio?.frequency,
- codec: audio?.codec,
- container: audio?.container,
- track: tag?.track,
- year: tag?.year
- };
- return song;
- }));
- const total = response.data?.total ?? songs.length;
- if (currentOffset + chunk.length >= total || chunk.length === 0) {
- break;
- }
- currentOffset += chunk.length;
- }
- return songs;
- }
- async getSongsPage(account: WebDavAccount, offset: number = 0, limit: number = 500): Promise<AudioStationPagedResponse<AudioStationSong>> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/song.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Song'),
- new QueryParam('version', '3'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_by', 'title'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('additional', 'song_tag,song_audio'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.postForm<AudioStationResponse<AudioStationSongListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取歌曲失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.songs ?? [];
- const items = chunk
- .filter(item => item.id)
- .map(item => {
- const audio = item.additional?.song_audio;
- const tag = item.additional?.song_tag;
- const duration = audio?.duration;
- const song: AudioStationSong = {
- id: item.id as string,
- title: item.title,
- path: item.path,
- album: tag?.album,
- albumArtist: tag?.album_artist,
- artist: tag?.artist,
- durationSeconds: duration ? Math.round(duration) : undefined,
- size: audio?.filesize,
- bitRate: audio?.bitrate,
- sampleRate: audio?.frequency,
- codec: audio?.codec,
- container: audio?.container,
- track: tag?.track,
- year: tag?.year
- };
- return song;
- });
- const total = response.data?.total ?? items.length;
- const nextStart = offset + items.length < total ? offset + items.length : null;
- const result: AudioStationPagedResponse<AudioStationSong> = {
- items,
- nextStart
- };
- return result;
- }
- async searchSongs(account: WebDavAccount, keyword: string, offset: number = 0, limit: number = 200): Promise<AudioStationSong[]> {
- const trimmed = keyword.trim();
- if (trimmed.length === 0) {
- return [];
- }
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/search.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Search'),
- new QueryParam('version', '1'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('keyword', trimmed),
- new QueryParam('sort_by', 'title'),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('additional', 'song_tag,song_audio'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.get<AudioStationResponse<AudioStationSearchData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 搜索失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.songs ?? [];
- const songs: AudioStationSong[] = [];
- for (let i = 0; i < chunk.length; i++) {
- const item = chunk[i];
- if (!item.id) {
- continue;
- }
- const audio = item.additional?.song_audio;
- const tag = item.additional?.song_tag;
- const duration = audio?.duration;
- const song: AudioStationSong = {
- id: item.id as string,
- title: item.title,
- path: item.path,
- album: tag?.album,
- albumArtist: tag?.album_artist,
- artist: tag?.artist,
- durationSeconds: duration ? Math.round(duration) : undefined,
- size: audio?.filesize,
- bitRate: audio?.bitrate,
- sampleRate: audio?.frequency,
- codec: audio?.codec,
- container: audio?.container,
- track: tag?.track,
- year: tag?.year
- };
- songs.push(song);
- }
- return songs;
- }
- async getPlaylistsPage(account: WebDavAccount, offset: number = 0, limit: number = 200): Promise<AudioStationPagedResponse<AudioStationPlaylist>> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/playlist.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Playlist'),
- new QueryParam('version', '2'),
- new QueryParam('method', 'list'),
- new QueryParam('library', 'all'),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.get<AudioStationResponse<AudioStationPlaylistListData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取歌单失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.playlists ?? [];
- const items = chunk
- .filter(item => item.id && item.name)
- .map(item => {
- const playlist: AudioStationPlaylist = {
- id: item.id as string,
- name: item.name as string,
- type: item.type,
- library: item.library,
- path: item.path
- };
- return playlist;
- });
- const total = response.data?.total ?? items.length;
- const nextStart = offset + items.length < total ? offset + items.length : null;
- const result: AudioStationPagedResponse<AudioStationPlaylist> = {
- items,
- nextStart
- };
- return result;
- }
- async getPlaylistSongsPage(
- account: WebDavAccount,
- playlistId: string,
- offset: number = 0,
- limit: number = 200
- ): Promise<AudioStationPagedResponse<AudioStationSong>> {
- const auth = await this.ensureAuth(account);
- const url = this.buildWebApiUrl(account, 'AudioStation/playlist.cgi');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Playlist'),
- new QueryParam('version', '2'),
- new QueryParam('method', 'getinfo'),
- new QueryParam('library', 'all'),
- new QueryParam('id', playlistId),
- new QueryParam('offset', `${offset}`),
- new QueryParam('limit', `${limit}`),
- new QueryParam('sort_direction', 'ASC'),
- new QueryParam('additional', 'songs_song_tag,songs_song_audio'),
- new QueryParam('_sid', auth.sid)
- ];
- const response = await this.get<AudioStationResponse<AudioStationPlaylistInfoData>>(url, params);
- if (!response.success) {
- throw new Error(`AudioStation 获取歌单歌曲失败(code=${response.error?.code ?? 'unknown'})`);
- }
- const chunk = response.data?.songs ?? [];
- const items = chunk
- .filter(item => item.id)
- .map(item => {
- const audio = item.additional?.song_audio;
- const tag = item.additional?.song_tag;
- const duration = audio?.duration;
- const song: AudioStationSong = {
- id: item.id as string,
- title: item.title,
- path: item.path,
- album: tag?.album,
- albumArtist: tag?.album_artist,
- artist: tag?.artist,
- durationSeconds: duration ? Math.round(duration) : undefined,
- size: audio?.filesize,
- bitRate: audio?.bitrate,
- sampleRate: audio?.frequency,
- codec: audio?.codec,
- container: audio?.container,
- track: tag?.track,
- year: tag?.year
- };
- return song;
- });
- const total = response.data?.total ?? items.length;
- const nextStart = offset + items.length < total ? offset + items.length : null;
- const result: AudioStationPagedResponse<AudioStationSong> = {
- items,
- nextStart
- };
- return result;
- }
- async buildSongCoverUrl(account: WebDavAccount, songId: string | undefined): Promise<string | undefined> {
- if (!songId) {
- return undefined;
- }
- const auth = await this.ensureAuth(account);
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Cover'),
- new QueryParam('version', '1'),
- new QueryParam('method', 'getsongcover'),
- new QueryParam('library', 'all'),
- new QueryParam('id', songId),
- new QueryParam('_sid', auth.sid)
- ];
- const url = this.buildWebApiUrl(account, 'AudioStation/cover.cgi');
- return `${url}?${this.buildQuery(params)}`;
- }
- async buildAlbumCoverUrl(account: WebDavAccount, albumName?: string, albumArtist?: string): Promise<string | undefined> {
- if (!albumName) {
- return undefined;
- }
- const auth = await this.ensureAuth(account);
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Cover'),
- new QueryParam('version', '1'),
- new QueryParam('method', 'getcover'),
- new QueryParam('library', 'all'),
- new QueryParam('album_name', albumName),
- new QueryParam('_sid', auth.sid)
- ];
- if (albumArtist) {
- params.push(new QueryParam('album_artist_name', albumArtist));
- }
- const url = this.buildWebApiUrl(account, 'AudioStation/cover.cgi');
- return `${url}?${this.buildQuery(params)}`;
- }
- async buildStreamUrl(account: WebDavAccount, songId: string): Promise<string> {
- if (!songId) {
- throw new Error('无效的AudioStation歌曲ID');
- }
- const auth = await this.ensureAuth(account);
- const shouldTranscode = songId.includes('_v_');
- const params = [
- new QueryParam('api', 'SYNO.AudioStation.Stream'),
- new QueryParam('version', '2'),
- new QueryParam('method', shouldTranscode ? 'transcode' : 'stream'),
- new QueryParam('id', songId),
- new QueryParam('_sid', auth.sid)
- ];
- if (shouldTranscode) {
- params.push(new QueryParam('format', 'mp3'));
- }
- const baseUrl = this.buildWebApiUrl(account, 'AudioStation/stream.cgi');
- const query = this.buildQuery(params);
- if (shouldTranscode) {
- return `${baseUrl}/0.mp3?${query}`;
- }
- return `${baseUrl}?${query}`;
- }
- private async ensureAuth(account: WebDavAccount): Promise<AudioStationAuthContext> {
- const key = this.buildCacheKey(account);
- const cached = this.authCache.get(key);
- if (cached?.sid) {
- return cached;
- }
- const auth = await this.login(account);
- this.authCache.set(key, auth);
- return auth;
- }
- private async login(account: WebDavAccount): Promise<AudioStationAuthContext> {
- if (!account.account || !account.password) {
- throw new Error('AudioStation 账号或密码为空');
- }
- const url = this.buildWebApiUrl(account, 'entry.cgi');
- const deviceId = this.getDeviceId();
- const params = [
- new QueryParam('api', 'SYNO.API.Auth'),
- new QueryParam('version', '6'),
- new QueryParam('method', 'login'),
- new QueryParam('session', 'audiostation'),
- new QueryParam('account', account.account),
- new QueryParam('passwd', account.password),
- new QueryParam('enable_device_token', 'yes'),
- new QueryParam('device_name', DEVICE_NAME),
- new QueryParam('device_id', deviceId)
- ];
- const response = await this.postForm<AudioStationResponse<AudioStationLoginResponseData>>(url, params);
- if (!response.success || !response.data?.sid) {
- const errorCode = response.error?.code ?? 'unknown';
- throw new Error(`AudioStation 登录失败(code=${errorCode})`);
- }
- const auth: AudioStationAuthContext = {
- sid: response.data.sid,
- did: response.data.did
- };
- void ServerLogUtil.info(TAG, `AudioStation 登录成功 sid=${auth.sid}`);
- return auth;
- }
- private buildCacheKey(account: WebDavAccount): string {
- return `${account.id ?? account.host}_${account.account}`;
- }
- private buildBaseUrl(account: WebDavAccount): string {
- const scheme = account.enableHttps ? 'https' : 'http';
- const port = account.port || (account.enableHttps ? 5001 : 5000);
- return `${scheme}://${account.host}:${port}`;
- }
- private buildWebApiUrl(account: WebDavAccount, path: string): string {
- const baseUrl = this.buildBaseUrl(account);
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
- return `${baseUrl}/webapi${normalizedPath}`;
- }
- private buildQuery(params: QueryParam[]): string {
- return params
- .filter(param => param.key && param.value !== undefined && param.value !== null)
- .map(param => `${encodeURIComponent(param.key)}=${encodeURIComponent(param.value)}`)
- .join('&');
- }
- private async get<T>(url: string, params: QueryParam[]): Promise<T> {
- const httpRequest = http.createHttp();
- const query = this.buildQuery(params);
- const response = await httpRequest.request(`${url}?${query}`, {
- method: http.RequestMethod.GET,
- header: {
- Accept: 'application/json'
- },
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING
- });
- if (response.responseCode !== 200) {
- httpRequest.destroy();
- throw new Error(`AudioStation 请求失败: HTTP ${response.responseCode}`);
- }
- httpRequest.destroy();
- return this.parseResponse<T>(response.result);
- }
- private async post<T>(url: string, params: QueryParam[], body?: AudioStationRequestBody): Promise<T> {
- const httpRequest = http.createHttp();
- const query = this.buildQuery(params);
- const response = await httpRequest.request(query ? `${url}?${query}` : url, {
- method: http.RequestMethod.POST,
- header: {
- Accept: 'application/json',
- 'Content-Type': 'application/json'
- },
- extraData: body ? JSON.stringify(body) : undefined,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING
- });
- if (response.responseCode !== 200) {
- httpRequest.destroy();
- throw new Error(`AudioStation 请求失败: HTTP ${response.responseCode}`);
- }
- httpRequest.destroy();
- return this.parseResponse<T>(response.result);
- }
- private async postForm<T>(url: string, params: QueryParam[]): Promise<T> {
- const httpRequest = http.createHttp();
- const body = this.buildQuery(params);
- const response = await httpRequest.request(url, {
- method: http.RequestMethod.POST,
- header: {
- Accept: 'application/json',
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- extraData: body,
- connectTimeout: 10000,
- readTimeout: 15000,
- expectDataType: http.HttpDataType.STRING
- });
- if (response.responseCode !== 200) {
- httpRequest.destroy();
- throw new Error(`AudioStation 请求失败: HTTP ${response.responseCode}`);
- }
- httpRequest.destroy();
- return this.parseResponse<T>(response.result);
- }
- private parseResponse<T>(payload: string | Object): T {
- if (typeof payload === 'string') {
- const trimmed = payload.trim();
- if (trimmed.length === 0) {
- return JSON.parse('{}') as T;
- }
- try {
- const parsed: JsonValue = JSON.parse(trimmed) as JsonValue;
- if (typeof parsed === 'string') {
- const inner = parsed.trim();
- if (inner.startsWith('{') || inner.startsWith('[')) {
- return JSON.parse(inner) as T;
- }
- }
- return parsed as T;
- } catch (_error) {
- return payload as T;
- }
- }
- return payload as T;
- }
- private getDeviceId(): string {
- const cached = PreferencesUtil.getStringSync(DEVICE_ID_KEY, '');
- if (cached && cached.length > 0) {
- return cached;
- }
- const id = `ttmusic_${Date.now().toString(36)}_${Math.floor(Math.random() * 100000)}`;
- PreferencesUtil.putSync(DEVICE_ID_KEY, id);
- return id;
- }
- }
- export const audioStationApi = new AudioStationApi();
|