wasdtv
Version:
WASD.tv API client
540 lines (494 loc) • 16.2 kB
text/typescript
import axios, { AxiosInstance, AxiosError } from 'axios'
import m3u8stream from 'm3u8stream'
import { PassThrough } from 'stream'
import { HttpError, ApiError } from '../core/error'
import { Wasd } from '../types/api'
export default class RestClient {
private _axios: AxiosInstance
constructor(private token: string) {
this._axios = axios.create({
baseURL: 'https://wasd.tv/api/',
headers: {
Authorization: `Token ${this.token}`,
},
})
}
public async getJWT(): Promise<Wasd.JWT> {
return this._axios
.post('auth/chat-token')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getChannelInfo(channel_name: string): Promise<Wasd.ChannelInfo> {
return this._axios
.get('v2/broadcasts/public', {
params: {
channel_name: channel_name,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getStreamChatMessages<T extends Wasd.InfoType>(
stream_id: number,
message_type: Wasd.MessageType,
limit = 500,
offset = 0,
): Promise<Wasd.RestMessage<T>[]> {
return this._axios
.get(`chat/streams/${stream_id}/messages`, {
params: {
type: message_type.toLocaleUpperCase(),
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async setChatSettings(stream_id: number, chat_role_limit: Wasd.ChatPermission, chat_delay: Wasd.ChatDelay): Promise<boolean> {
return this._axios
.post(`chat/streams/${stream_id}/settings`, {
chatRoleLimitMode: chat_role_limit,
chatDelayLimitMode: chat_delay,
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async addModerator(channel_id: number, user_id: number): Promise<boolean> {
return this._axios
.put(`channels/${channel_id}/moderators`, { user_id: user_id })
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async deleteModerator(channel_id: number, user_id: number): Promise<boolean> {
return this._axios
.delete(`api/channels/${channel_id}/moderators/${user_id}`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async listModerator(streamer_id: number): Promise<Wasd.Moderator[]> {
return this._axios
.get(`chat/streamers/${streamer_id}/moderators`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async banUser(channel_id: number, user_id: number, stream_id: number, keep_messages = true, duration = 10): Promise<boolean> {
return this._axios
.put(`api/channels/${channel_id}/banned-users`, {
user_id: user_id,
stream_id: stream_id,
keep_messages: keep_messages,
duration: duration,
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async unbanUser(channel_id: number, user_id: number): Promise<boolean> {
return this._axios
.delete(`api/channels/${channel_id}/users/${user_id}`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async listBanned(streamer_id: number): Promise<Wasd.BannedUser[]> {
return this._axios
.get(`chat/streamers/${streamer_id}/ban`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getGifts(media_container_id: number): Promise<Wasd.Gift[]> {
return this._axios
.get('v2/gifts', {
params: {
media_container_id: media_container_id,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async productsToSell(user_id: number): Promise<Wasd.Product[]> {
return this._axios
.get('v2/shop/products-to-sell', {
params: {
user_id: user_id,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getWebCustomBlocks(channel_id: number): Promise<Wasd.WebCustomBlock[]> {
return this._axios
.get(`v2/channels/${channel_id}/custom_blocks`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getStickerPack(streamer_id: number, limit = 100, offset = 0): Promise<Wasd.StickerPack[]> {
return this._axios
.get(`api/chat/streamers/${streamer_id}/stickerpack`, {
params: {
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getPopularClips(): Promise<Wasd.Clip[]> {
return this._axios
.get(`v2/main/clips/popular`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getClips(channel_id: number, limit: number, offset: number): Promise<Wasd.Clip[]> {
return this._axios
.get('v2/clips', {
params: {
clip_channel_id: channel_id,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getGames(limit = 20, offset = 0): Promise<Wasd.Game[]> {
return this._axios
.get('games', {
params: {
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getMediaContainers(
media_container_status?: Wasd.MediaStatus,
media_container_type?: Wasd.MediaContainerType,
game_id?: number,
limit = 20,
offset = 0,
): Promise<Wasd.MediaContainerExtra[]> {
return this._axios
.get(`v2/media-containers`, {
params: {
media_container_status: media_container_status,
media_container_type: media_container_type,
game_id: game_id,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getChannelMediaContainers(
media_container_status?: Wasd.MediaStatus,
media_container_type?: Wasd.MediaContainerType,
channel_id?: number,
limit = 20,
offset = 0,
): Promise<Wasd.MediaContainerExtra[]> {
return this._axios
.get(`v2/media-containers/plain`, {
params: {
media_container_status: media_container_status,
media_container_type: media_container_type,
channel_id: channel_id,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getProfile(): Promise<Wasd.User> {
return this._axios
.get('v2/profiles/current')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getNotifications(): Promise<Wasd.Notification[]> {
return this._axios
.get('v2/notifications/bell')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getChannelLinks(channel_id: number): Promise<Wasd.ChannelLink[]> {
return this._axios
.get(`channels/${channel_id}/links`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async searchGames(search_phrase: string, limit: number, offset: number): Promise<Wasd.SearchResult<Wasd.Game>> {
return this._axios
.get('search/games', {
params: {
search_phrase: search_phrase,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async searchProfile(search_phrase: string, limit: number, offset: number): Promise<Wasd.SearchResult<Wasd.UserProfile>> {
return this._axios
.get('search/profiles', {
params: {
search_phrase: search_phrase,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async searchChannel(search_phrase: string, limit: number, offset: number): Promise<Wasd.SearchResult<Wasd.Channel>> {
return this._axios
.get('search/channels', {
params: {
search_phrase: search_phrase,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async searchMediaContainer(
media_container_name?: string,
media_container_status?: Wasd.MediaStatus,
limit = 20,
offset = 0,
): Promise<Wasd.SearchResult<Wasd.MediaContainerExtra>> {
return this._axios
.get('v2/search/media-containers', {
params: {
media_container_name: media_container_name,
media_container_status: media_container_status,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getTags(type?: Wasd.TagType, limit = 30, offset = 0): Promise<Wasd.Tag[]> {
return this._axios
.get('v2/tags', {
params: {
type: type,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getStreamPushUrl(): Promise<Wasd.StreamPushUrl> {
return this._axios
.get('auth/users/current/stream-push-url')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getStreamClosedViewUrl(): Promise<Wasd.StreamClosedViewUrl> {
return this._axios
.get('v2/profiles/current/broadcasts/closed-view-url')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getBroadcastLimits(): Promise<Wasd.BroadcastLimits> {
return this._axios
.get('v2/broadcast-limits')
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
// TODO: investigate possible search_value types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async setSetting(setting_key: Wasd.SettingKey, setting_value: any): Promise<Wasd.Setting[]> {
return this._axios
.patch('profiles/current/settings', {
new_settings: [
{
setting_key: setting_key,
setting_value: setting_value,
},
],
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getPosts(user_id: number, limit: number, offset: number): Promise<Wasd.Post[]> {
return this._axios
.get('v2/posts', {
params: {
user_id: user_id,
limit: limit,
offset: offset,
},
})
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
public async getSchedule(user_id: number): Promise<Wasd.Schedule[]> {
return this._axios
.get(`schedules/weekly/${user_id}`)
.then(({ data }) => {
return data.result
})
.catch((err) => this.errorHandler(err))
}
// TODO: add options to method
public downloadLiveMediaStream(user_id: number): PassThrough {
return this.downloadMediaByUrl(`https://cdn-curie.wasd.tv/live/${user_id}/tracks-v1a1/mono.m3u8`)
}
public downloadMediaByUrl(url: string): PassThrough {
return m3u8stream(url)
}
public downloadVod(media_container: Wasd.MediaContainer): PassThrough {
if (
media_container.media_container_status !== 'STOPPED' &&
media_container.media_container_type !== 'SINGLE' &&
media_container.media_container_online_status !== 'PUBLIC'
) {
throw new ApiError('MediaContanier must have status STOPPED and type SINGLE')
}
const archive_url = media_container.media_container_streams[0]?.stream_media[0]?.media_meta.media_archive_url
if (archive_url === undefined) {
throw new ApiError('Unable to fetch media url')
}
const download_url = archive_url?.replace('/index-', '/tracks-v1a1/index-').replace('cdn.wasd.tv', 'cdn-volta.wasd.tv') as string
return this.downloadMediaByUrl(download_url)
}
public async getMediaStreamMetadata(user_id: number): Promise<Wasd.MediaStreamMetadata> {
return axios
.get(`https://cdn.wasd.tv/live/${user_id}/index.m3u8`)
.then(({ data }) => {
const parse = {
bandwidth: data
.match(/(,BANDWIDTH=[0-9]*)/gm)
?.at(0)
?.replace(',BANDWIDTH=', ''),
average_bandwidth: data
.match(/(,AVERAGE-BANDWIDTH=[0-9]*)/gm)
.at(0)
?.replace(',AVERAGE-BANDWIDTH=', ''),
codecs: data
.match(/(,CODECS=".*")/gm)
?.at(0)
?.replace(',CODECS="', '')
.replace('"', ''),
resolution: data
.match(/(,RESOLUTION=[0-9]*x[0-9]*)/gm)
?.at(0)
?.replace(',RESOLUTION=', ''),
closed_captions: data
.match(/(,CLOSED-CAPTIONS=[A-Z]*)/gm)
?.at(0)
?.replace(',CLOSED-CAPTIONS=', ''),
frame_rate: data
.match(/(,FRAME-RATE=[0-9]*\.[0-9]*)/gm)
?.at(0)
?.replace(',FRAME-RATE=', ''),
source_url: data.match(/(https:\/\/.*m3u8)/gm)?.at(0),
}
return {
bandwidth: parse.bandwidth ? Number(parse.bandwidth) : undefined,
average_bandwidth: parse.average_bandwidth ? Number(parse.average_bandwidth) : undefined,
codecs: parse.codecs,
resolution: parse.resolution,
closed_captions: parse.closed_captions,
frame_rate: parse.frame_rate ? Number(parse.frame_rate) : undefined,
source_url: parse.source_url,
}
})
.catch((err) => {
this.errorHandler(err)
return {
bandwidth: undefined,
average_bandwidth: undefined,
codecs: undefined,
resolution: undefined,
closed_captions: undefined,
frame_rate: undefined,
source_url: undefined,
}
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private errorHandler(err: any) {
if (err instanceof AxiosError) {
if (err.response?.data?.error !== undefined) {
throw new ApiError(JSON.stringify(err.response.data.error, null, 2))
} else {
console.log(err)
throw new HttpError(err.message)
}
}
}
}