mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2024-12-22 05:12:57 -05:00
Create twitch IRC client and begin to implement a HLS proxy
This commit is contained in:
parent
0c084d4f09
commit
3a09ec5112
4 changed files with 136 additions and 0 deletions
|
@ -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
|
17
server/types/scraping/Chat.ts
Normal file
17
server/types/scraping/Chat.ts
Normal 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
|
||||
}
|
65
server/util/scraping/chat/chat.ts
Normal file
65
server/util/scraping/chat/chat.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
4
server/util/scraping/chat/utils.ts
Normal file
4
server/util/scraping/chat/utils.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const parseUsername = (rawUsername: String) => {
|
||||
const splitted = rawUsername.split('!')
|
||||
return splitted[0]
|
||||
}
|
Loading…
Reference in a new issue