Create twitch IRC client and begin to implement a HLS proxy

This commit is contained in:
dragongoose 2023-03-13 21:07:25 -04:00
parent 0c084d4f09
commit 3a09ec5112
4 changed files with 136 additions and 0 deletions

View file

@ -1,3 +1,4 @@
import { Streamlink } from '@dragongoose/streamlink';
import { Router, Response, Request, NextFunction } from 'express'
const proxyRouter = Router();
@ -24,4 +25,53 @@ proxyRouter.get('/img', async (req: Request, res: Response, next: NextFunction)
.catch((err) => next(err))
})
proxyRouter.get('/stream/:username/hls.m3u8', (req: Request, res: Response, next: NextFunction) => {
console.log(req.params.username)
const streamlink = new Streamlink(`https://twitch.tv/${req.params.username}`, {
otherArgs: ['--stream-url']
})
streamlink.begin()
streamlink.on('log', async (data) => {
// m3u8 url
let twitchM3u8url = data.toString()
const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
const twitchRes = await fetch(twitchM3u8url)
let m3u8Data = await twitchRes.text()
const matches = m3u8Data.match(urlRegex)
if (!matches) return next(new Error('Error proxying HLS'));
for (let url of matches) {
const base64data = Buffer.from(url).toString('base64url')
//m3u8Data = m3u8Data.replace(url, `${process.env.URL}/proxy/hls/${base64data}`)
}
res.setHeader('Content-type','application/vnd.apple.mpegurl')
res.send(m3u8Data)
})
})
proxyRouter.get('/hls/:encodedUrl' , (req: Request, res: Response, next: NextFunction) => {
const unencodedUrl = Buffer.from(req.params.encodedUrl, 'base64url').toString()
fetch(unencodedUrl).then((response) => {
response.body!.pipeTo(
new WritableStream({
start() {
response.headers.forEach((v, n) => res.setHeader(n, v));
},
write(chunk) {
res.write(chunk);
},
close() {
res.end();
},
})
);
})
.catch((err) => next(err))
})
export default proxyRouter

View file

@ -0,0 +1,17 @@
export interface TwitchChatOptions {
login: {
username: string,
password: string
},
channels: string[]
}
export const MessageTypes = ['PRIVMSG', 'WHISPER']
export type MessageType = typeof MessageTypes[number];
export interface Metadata {
username: string
messageType: MessageType
channel: string
message: string
}

View file

@ -0,0 +1,65 @@
import { EventEmitter } from 'stream';
import WebSocket from 'ws'
import { TwitchChatOptions, Metadata, MessageType, MessageTypes } from '../../../types/scraping/Chat'
import { parseUsername } from './utils';
export declare interface TwitchChat {
on(event: 'PRIVMSG', listener: (username: string, messageType: MessageType, channel: string, message: string) => void): this
}
export class TwitchChat extends EventEmitter{
public channels: string[]
private url = 'wss://irc-ws.chat.twitch.tv:443'
private ws: WebSocket | null;
constructor(options: TwitchChatOptions) {
super()
this.channels = options.channels
this.ws = null
}
private parser() {
this.ws?.on('message', (data) => {
let normalData = data.toString()
let splitted = normalData.split(":")
let metadata = splitted[1].split(' ')
let message = splitted[2]
if(!MessageTypes.includes(metadata[1])) return;
let parsedMetadata: Metadata = {
username: parseUsername(metadata[0]),
messageType: metadata[1],
channel: metadata[2],
message: message
}
this.createEmit(parsedMetadata)
})
}
private createEmit(data: Metadata) {
this.emit(data.messageType, ...Object.values(data))
}
public async connect() {
this.ws = new WebSocket(this.url)
this.ws.on('open', () => {
if(this.ws) {
this.ws.send('PASS none')
this.ws.send('NICK justinfan333333333333')
for(let channel of this.channels) {
this.ws.send(`JOIN #${channel}`)
}
this.parser()
return Promise.resolve()
}
})
}
}

View file

@ -0,0 +1,4 @@
export const parseUsername = (rawUsername: String) => {
const splitted = rawUsername.split('!')
return splitted[0]
}