Add Stream API

This commit is contained in:
patrickkfkan 2023-10-30 01:13:03 +08:00
parent e6a01cbe85
commit 7bb1899029
8 changed files with 95 additions and 3 deletions

View File

@ -0,0 +1,26 @@
import bcfetch from '../../';
const streamUrl = 'https://t4.bcbits.com/stream/a9fab205c6d82ffc4ca4f377d86bd8dd/mp3-128/3535427082?p=0&ts=1698526235&t=3c987d3b75dc4151c931288780eca682ae1d70ac&token=1698526235_35d7ab2f75c60cdb31c68df15cc5d5024368a535';
console.log(`Testing stream URL: ${streamUrl}`);
bcfetch.stream.test(streamUrl).then((result1) => {
if (result1.ok) {
console.log('Stream URL is valid.');
return;
}
console.log(`Stream URL is invalid. Status code: ${result1.status}. Refreshing...`);
bcfetch.stream.refresh(streamUrl).then((result2) => {
console.log(`Refreshed stream URL: ${result2}`);
if (result2) {
console.log('Testing refreshed stream URL...');
bcfetch.stream.test(result2).then((result3) => {
if (result3.ok) {
console.log('Refreshed stream URL is valid.');
return;
}
console.log(`Refreshed stream URL is invalid. Status code: ${result3.status}`);
});
}
});
});

View File

@ -0,0 +1,5 @@
Testing stream URL: https://t4.bcbits.com/stream/a9fab205c6d82ffc4ca4f377d86bd8dd/mp3-128/3535427082?p=0&ts=1698526235&t=3c987d3b75dc4151c931288780eca682ae1d70ac&token=1698526235_35d7ab2f75c60cdb31c68df15cc5d5024368a535
Stream URL is invalid. Status code: 410. Refreshing...
Refreshed stream URL: https://t4.bcbits.com/stream/a9fab205c6d82ffc4ca4f377d86bd8dd/mp3-128/3535427082?p=0&ts=1698685897&t=933f95e7071852f98c44ce1876d4a68a29cc229a&token=1698685897_e04500be84f7e94b43baed9170b689cfda72d565
Testing refreshed stream URL...
Refreshed stream URL is valid.

View File

@ -13,6 +13,7 @@ export { default as TagAPI } from './lib/tag/TagAPI.js';
export { default as TrackAPI } from './lib/track/TrackAPI.js';
export { default as SearchAPI } from './lib/search/SearchAPI.js';
export { default as AutocompleteAPI } from './lib/autocomplete/AutocompleteAPI.js';
export { default as StreamAPI } from './lib/stream/StreamAPI.js';
export * from './lib/common/BaseAPI.js';
export * from './lib/common/BaseAPIWithImageSupport.js';
@ -28,6 +29,7 @@ export * from './lib/track/TrackAPI.js';
export * from './lib/album/AlbumAPI.js';
export * from './lib/search/SearchAPI.js';
export * from './lib/autocomplete/AutocompleteAPI.js';
export * from './lib/stream/StreamAPI.js';
export * from './lib/types/Album.js';
export * from './lib/types/Article.js';

View File

@ -13,6 +13,7 @@ import TrackAPI, { LimiterTrackAPI } from './track/TrackAPI.js';
import Cache, { CacheDataType } from './utils/Cache.js';
import Fetcher from './utils/Fetcher.js';
import Limiter from './utils/Limiter.js';
import StreamAPI, { LimiterStreamAPI } from './stream/StreamAPI.js';
export interface BandcampFetchParams {
cookie?: string | null;
@ -37,6 +38,7 @@ export default class BandcampFetch {
readonly fan: FanAPI;
readonly search: SearchAPI;
readonly autocomplete: AutocompleteAPI;
readonly stream: StreamAPI;
readonly limiter: {
readonly album: LimiterAlbumAPI;
@ -50,6 +52,7 @@ export default class BandcampFetch {
readonly fan: LimiterFanAPI;
readonly search: LimiterSearchAPI;
readonly autocomplete: LimiterAutocompleteAPI;
readonly stream: StreamAPI;
updateSettings: (options?: Bottleneck.ConstructorOptions) => void;
};
@ -89,6 +92,7 @@ export default class BandcampFetch {
this.fan = new FanAPI(baseAPIWithImageSupportParams);
this.search = new SearchAPI(baseAPIWithImageSupportParams);
this.autocomplete = new AutocompleteAPI(baseAPIParams);
this.stream = new StreamAPI(baseAPIParams);
this.limiter = {
album: new LimiterAlbumAPI(baseAPIWithImageSupportParams),
@ -102,6 +106,7 @@ export default class BandcampFetch {
fan: new LimiterFanAPI(baseAPIWithImageSupportParams),
search: new LimiterSearchAPI(baseAPIWithImageSupportParams),
autocomplete: new LimiterAutocompleteAPI(baseAPIParams),
stream: new LimiterStreamAPI(baseAPIParams),
updateSettings: this.#limiter.updateSettings.bind(this.#limiter)
};
}

View File

@ -1,3 +1,4 @@
import { Response } from 'node-fetch';
import Cache from '../utils/Cache.js';
import Fetcher, { FetchMethod } from '../utils/Fetcher.js';
@ -16,9 +17,10 @@ export default abstract class BaseAPI {
this.#cache = params.cache;
}
protected fetch(url: string, jsonResponse: false, method: FetchMethod.HEAD, payload?: undefined): Promise<Response>;
protected fetch(url: string, jsonResponse: true, method?: FetchMethod, payload?: Record<string, any>): Promise<any>;
protected fetch(url: string, jsonResponse?: boolean, method?: FetchMethod, payload?: Record<string, any>): Promise<string>;
protected fetch(url: string, jsonResponse?: boolean, method?: FetchMethod, payload?: Record<string, any>) {
protected fetch(url: string, jsonResponse?: boolean, method?: FetchMethod, payload?: Record<string, any>): Promise<any> {
return this.#fetcher.fetch(url, jsonResponse, method, payload);
}

View File

@ -0,0 +1,44 @@
import BaseAPI, { BaseAPIParams } from '../common/BaseAPI.js';
import { URLS } from '../utils/Constants.js';
import { FetchMethod } from '../utils/Fetcher.js';
import Limiter from '../utils/Limiter.js';
export interface StreamTestResult {
ok: boolean;
status: number;
}
export default class StreamAPI extends BaseAPI {
async test(url: string): Promise<StreamTestResult> {
const res = await this.fetch(url, false, FetchMethod.HEAD);
return {
ok: res.ok,
status: res.status
};
}
async refresh(url: string): Promise<string | null> {
const refreshUrl = new URL(URLS.REFRESH_STREAM);
refreshUrl.searchParams.set('url', url);
const res = await this.fetch(refreshUrl.toString(), true);
if (res && res.url && typeof res.url === 'string') {
return res.url;
}
return null;
}
}
export class LimiterStreamAPI extends StreamAPI {
#limiter: Limiter;
constructor(params: BaseAPIParams & { limiter: Limiter }) {
super(params);
this.#limiter = params.limiter;
}
async refresh(url: string): Promise<string | null> {
return this.#limiter.schedule(() => super.refresh(url));
}
}

View File

@ -17,5 +17,6 @@ export const URLS = {
AUTOCOMPLETE: {
TAG: `${API_URL}/fansignup/1/search_tag`,
LOCATION: `${API_URL}/location/1/geoname_search`
}
},
REFRESH_STREAM: `${API_URL}/stream/1/refresh`
};

View File

@ -4,7 +4,8 @@ import Cache, { CacheDataType } from './Cache.js';
export enum FetchMethod {
GET = 'GET',
POST = 'POST'
POST = 'POST',
HEAD = 'HEAD'
}
export interface FetcherParams {
@ -34,6 +35,7 @@ export default class Fetcher {
return this.#cookie;
}
fetch(url: string, jsonResponse: false, method: FetchMethod.HEAD, payload?: undefined): Promise<Response>;
fetch(url: string, jsonResponse: true, method?: FetchMethod, payload?: Record<string, any>): Promise<any>;
fetch(url: string, jsonResponse?: boolean, method?: FetchMethod, payload?: Record<string, any>): Promise<string>;
fetch(url: string, jsonResponse?: boolean, method?: FetchMethod, payload?: Record<string, any>) {
@ -44,6 +46,11 @@ export default class Fetcher {
if (method === undefined) {
method = FetchMethod.GET;
}
if (method === FetchMethod.HEAD) {
return fetch(url, { method: 'HEAD' });
}
let response;
if (method === FetchMethod.GET) {
const urlObj = new URL(url);