diff --git a/README.md b/README.md index 2f0a2ac..4334c6c 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ npm run serve - expires_in - tokenSignDate - mid + - cookies - **streamerInfo**: 一个数组,描述录制信息。 - **name**: 主播名。 - uploadLocalFile(true): 是否投稿。 @@ -210,7 +211,8 @@ npm run serve "refresh_token": "", "expires_in": 0, "tokenSignDate": 0, - "mid": 0 + "mid": 0, + "cookies": "" }, "streamerInfo": [ { diff --git a/src/engine/website/bilibili.ts b/src/engine/website/bilibili.ts index a8c838b..d1c9361 100644 --- a/src/engine/website/bilibili.ts +++ b/src/engine/website/bilibili.ts @@ -1,37 +1,68 @@ -import { getExtendedLogger } from "../../log"; - const axios = require("axios"); export function main(url: string) { return new Promise(function (resolve, reject) { const rid: any = url.match(/(?<=\/)\d{2,}/g); - axios - .get( - `https://api.live.bilibili.com/room/v1/Room/room_init?id=${rid}` - ) + const roomInitUrl = `https://api.live.bilibili.com/room/v1/Room/room_init?id=${rid}` + const roomPlayInfoUrl = `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo` + const config: any = { + method: "get", + url: '', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${Math.floor(Math.random() * (120 - 100 + 1)) + 100}.0.0.0 Safari/537.36`, + // 由于 bilibili 限制,需登录才可获取最高码率 + 'Cookie': `${global.app.user.cookies}`, + 'Referer': url + }, + params: {} + }; + config.url = roomInitUrl + axios(config) .then(function (response: any) { const data: any = response.data; - if (data["code"] != 0 || data["data"]["live_time"] < 0) { + if (data["code"] != 0 || data["data"]["live_status"] != 1) { reject( "BILIBILI=>No match results:Maybe the roomid is error,or this room is not open!" ); } const real_id: string = data["data"]["room_id"]; - const config: any = { - method: "get", - url: `https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?cid=${real_id}&platform=h5&qn=10000`, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }; - - axios(config).then(function (response: any) { + config.url = roomPlayInfoUrl + config.params = { + 'room_id': real_id, + 'protocol': '0,1', // 0: http_stream, 1: http_hls + 'format': '0,1,2', + 'codec': '0,1', + 'qn': 10000, + 'platform': 'html5', // web, html5, android, ios + 'dolby': '5', + } + axios(config) + .then(function (response: any) { const html: any = response.data; - getExtendedLogger("bilibili").trace(`获取哔哩哔哩房间 ${rid} 的推流信息 ${JSON.stringify(html, null, 2)}`) - const links: any = html["data"]["durl"]; - let m3u8_url = links[0]["url"]; + if (html['code'] != 0) { + reject( + "BILIBILI=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } + const streamInfoList: any = html['data']['playurl_info']['playurl']['stream'] + let m3u8_url: string = '' + streamInfoList.forEach((streamInfo: any) => { + if (streamInfo['format'][0]['format_name'] === 'flv') { + // 选取最高码率 + const codec = streamInfo['format'][0]['codec'].reduce((prev: any, curr: any) => { + return (prev.current_qn >= curr.current_qn) ? prev : curr; + }) + const baseUrl = codec['base_url'] + const urlInfo: any = codec['url_info'][0] + m3u8_url = urlInfo['host'] + baseUrl + urlInfo['extra'] + } + }) resolve(m3u8_url); - }); + }) + .catch((error: any) => { + reject(error) + }) }) .catch(function (error: any) { reject(error); diff --git a/src/engine/website/huajiao.ts b/src/engine/website/huajiao.ts index ee0f1e6..11581c0 100644 --- a/src/engine/website/huajiao.ts +++ b/src/engine/website/huajiao.ts @@ -1,25 +1,52 @@ const axios = require("axios"); export function main(url: string) { - return new Promise(function (resolve, reject) { - const rid: any = url.match(/[0-9]+/g); - const tt: any = Date.now(); - axios - .get( - `https://h.huajiao.com/api/getFeedInfo?sid=${tt}&liveid=${rid}` - ) - .then(function (response: any) { - const jsons: any = response.data; - if (jsons["data"]) { - resolve(jsons["data"]["live"]["main"]); - } else { + return new Promise((resolve, reject) => { + if (url.indexOf('user') != -1) { + // 主页地址,可以多次获取直播流 + const uid = url.match(/(?<=user\/)(\d+)/g)![0]; + axios.get(`https://webh.huajiao.com/User/getUserFeeds?_callback=padding&uid=${uid}&fmt=jsonp&_=${Date.now()}`) + .then((response: any) => { + const data = response.data.replace('/**/padding(', '').replace(');','') + const feedInfos = JSON.parse(data).data['feeds'] + const feedInfo = feedInfos.find((feedInfo: any) => feedInfo.type === 1) + if (!feedInfo || !feedInfo['relay']) { reject( "HUAJIAO=>No match results:Maybe the roomid is error,or this room is not open!" ); } + const streamUrl: string = `http://al2-flv.live.huajiao.com/${feedInfo['relay']['channel']}/${feedInfo['feed']['sn']}.flv` + resolve(streamUrl) }) - .catch(function (error: any) { + .catch((error: any) => { reject(error); - }); + }) + } else if (url.indexOf('l') != -1) { + // 当次直播地址,只能获取该次直播的直播流 + const rid = url.match(/(?<=l\/)(\d+)/g)![0]; + axios.get(`https://www.huajiao.com/l/${rid}`) + .then((response: any) => { + const data: any = response.data; + const feedInfo: any = JSON.parse(data.split('var feed = ')[1].split(';\n')[0]) + const uid = feedInfo['author']['uid'] + // 提醒用户更换主页链接 + global.app.logger.warn(`HUAJIAO => roomUrl: ${url} 只能录制当次直播,请更换 roomUrl 为 https://www.huajiao.com/user/${uid}`) + if (feedInfo && feedInfo['feed']['duration'] != '00:00:00') { + reject( + "HUAJIAO=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } + const streamUrl: string = `http://al2-flv.live.huajiao.com/${feedInfo['relay']['channel']}/${feedInfo['feed']['sn']}.flv` + resolve(streamUrl) + }) + .catch((error: any) => { + global.app.logger.warn(`HUAJIAO => roomUrl: ${url} 只能录制当次直播,请更换 roomUrl 为 https://www.huajiao.com/user/xxxx`) + reject(error); + }); + } else { + reject( + "HUAJIAO=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } }); } diff --git a/src/engine/website/kuaishou.ts b/src/engine/website/kuaishou.ts index f3f8f30..47fc85d 100644 --- a/src/engine/website/kuaishou.ts +++ b/src/engine/website/kuaishou.ts @@ -3,32 +3,57 @@ const axios = require("axios"); export function main(url: string) { return new Promise(function (resolve, reject) { const rid: any = url.match(/(?<=u\/)(.+)/g); - - const config: any = { - method: "get", - url: `https://m.gifshow.com/fw/live/${rid[0]}`, + const session = axios.create({ + baseURL: 'https://live.kuaishou.com', headers: { - "user-agent": - "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1", - cookie: "did=web_", - }, - }; - - axios(config) - .then(function (response: any) { - const html: any = response.data; - const reg: RegExp = /(?<=type="application\/x-mpegURL" src=")(.+?)(?=")/; - const strs: any = html.match(reg); - if (strs && strs.length >= 1) { - resolve(strs[0].replace(/#38;/g, "")); - } else { - reject( - "KUAISHOU=>No match results:Maybe the roomid is error,or this room is not open!" - ); - } + 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${Math.floor(Math.random() * (120 - 100 + 1)) + 100}.0.0.0 Safari/537.36` + } + }) + // 首页低风控生成 did cookie + session.get('/') + .then(() => { + const delay = Math.floor(Math.random() * 1000) + 3000 + // 延迟3-4秒以防风控 + setTimeout(() => { + session.get(`/u/${rid}`) + .then((response: any) => { + const errorKeys = ['错误代码22', '主播尚未开播'] + errorKeys.forEach((errorKey) => { + if (response.data.indexOf(errorKey) != -1) { + reject( + "KUAISHOU=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } + }) + session.get(`/live_api/liveroom/livedetail?principalId=${rid}`) + .then((response: any) => { + const roomInfo: any = response.data.data + if (roomInfo['result'] == 1) { + const streamInfoList: any = roomInfo['liveStream']['playUrls'][0]['adaptationSet']['representation'] + // 不登录只能录制标清码率? ===> 只在前端限制了 + // 录制最高码率 + const streamInfo = streamInfoList.reduce((prev: any, curr: any) => { + return (prev.bitrate > curr.bitrate) ? prev : curr; + }) + const streamUrl: string = streamInfo['url'] + resolve(streamUrl) + } else { + reject( + "KUAISHOU=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } + }) + .catch((error: any) => { + reject(error) + }) + }) + .catch((error: any) => { + reject(error) + }) + }, delay) }) - .catch(function (error: any) { + .catch((error: any) => { reject(error); - }); + }) }); } \ No newline at end of file diff --git a/src/engine/website/kugou.ts b/src/engine/website/kugou.ts index 18ec3ab..0cb34ca 100644 --- a/src/engine/website/kugou.ts +++ b/src/engine/website/kugou.ts @@ -2,23 +2,56 @@ const axios = require("axios"); export function main(url: string) { return new Promise(function (resolve, reject) { - const rid: any = url.match(/[0-9]+(?=\?)*/g); - axios - .get( - `https://fx1.service.kugou.com/video/pc/live/pull/v3/streamaddr?roomId=${rid[0]}&ch=fx&version=1.0&streamType=1-2-5&platform=7&ua=fx-flash&kugouId=0&layout=1` - ) - .then(function (response: any) { - const json: any = response.data; - if (json && json["code"] == 0) { - resolve(json["data"]["horizontal"][0]["httpflv"][0]); - } else { - reject( - "KUGOU=>No match results:Maybe the roomid is error,or this room is not open!" - ); + const rid: any = url.match(/[0-9]+(?=\?)*/)![0]; + axios.get(`https://fanxing.kugou.com/${rid}`) + .then((response: any) => { + const match: any = response.data.match(/roomId=(\d+)/) + let roomId: string = rid; + if (match) { + roomId = match[1] } + const config: any = { + method: "get", + url: `https://fx1.service.kugou.com/video/pc/live/pull/mutiline/streamaddr?std_rid=${roomId}`, + headers: { + 'Content-Type': 'application/json;charset=utf-8', + 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${Math.floor(Math.random() * (120 - 100 + 1)) + 100}.0.0.0 Safari/537.36`, + 'Referer': url, + }, + params: { + appid: '1010', + version: '1000', + std_plat: 7, + std_kid: 0, + std_imei: '', + streamType: '1-2-4-5-8', + targetLiveTypes: '1-5-6', + ua: 'fx-flash', + supportEncryptMode: 1, + _: Date.now() + } + } + axios(config) + .then((response: any) => { + const html: any = response.data; + if (html['code'] != 0 || html.data['lines'].length == 0) { + reject( + "KUGOU=>No match results:Maybe the roomid is error,or this room is not open!" + ) + } + const streamInfoList: any = html.data['lines'].pop()['streamProfiles'] + const streamInfo = streamInfoList.reduce((prev: any, curr: any) => { + return (prev.rate > curr.rate) ? prev : curr; + }) + const streamUrl: string = streamInfo['httpsFlv'][0] + resolve(streamUrl) + }) + .catch((error: any) => { + reject(error) + }) + }) + .catch((error: any) => { + reject(error) }) - .catch(function (error: any) { - reject(error); - }); }); } diff --git a/src/engine/website/yy.ts b/src/engine/website/yy.ts index 2002511..bd4b596 100644 --- a/src/engine/website/yy.ts +++ b/src/engine/website/yy.ts @@ -8,25 +8,74 @@ export function main(url: string) { "YY=>No match results:Maybe the roomid is error,or this room is not open!" ); } + const secTime = Math.trunc(Date.now() / 1000) + const milTime = Date.now() + const data: any = { + head: { + "seq": milTime, + "appidstr": "0", + "bidstr": "121", + "cidstr": rid[0], + "sidstr": rid[0], + "uid64": 0, + "client_type": 108, + "client_ver": "5.11.0-alpha.4", + "stream_sys_ver": 1, + "app": "yylive_web", + "playersdk_ver": "5.11.0-alpha.4", + "thundersdk_ver": "0", + "streamsdk_ver": "5.11.0-alpha.4" + }, + client_attribute: { + "client": "web", + "model": "", + "cpu": "", + "graphics_card": "", + "os": "chrome", + "osversion": "114.0.0.0", + "vsdk_version": "", + "app_identify": "", + "app_version": "", + "business": "", + "width": "1536", + "height": "864", + "scale": "", + "client_type": 8, + "h265": 0 + }, + avp_parameter: { + "version": 1, + "client_type": 8, + "service_type": 0, + "imsi": 0, + "send_time": secTime, + "line_seq": -1, + "gear": 4, + "ssl": 1, + "stream_format": 0 + } + } const config: any = { - method: "get", - url: `http://interface.yy.com/hls/new/get/${rid[0]}/${rid[0]}/1200?source=wapyy&callback=jsonp3`, + method: "post", + url: `https://stream-manager.yy.com/v3/channel/streams?uid=0&cid=${rid[0]}&sid=${rid[0]}&appid=0&sequence=${milTime}&encode=json`, headers: { "Content-Type": "application/json; charset=utf-8", - referer: `http://wap.yy.com/mobileweb/${rid[0]}`, - "user-agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", + 'Referer': `https://www.yy.com/`, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${Math.floor(Math.random() * (120 - 100 + 1)) + 100}.0.0.0 Safari/537.36", }, + data: data }; - axios(config) .then(function (response: any) { - const html: string = response.data; - const jsons: any = html.match(/\(([\W\w]*)\)/); - const flv_url: any = JSON.parse( - jsons[0].replace("(", "").replace(")", "") - ); - resolve(flv_url["hls"]); + const data: any = response.data; + if (!data || !data['avp_info_res']) { + reject( + "YY=>No match results:Maybe the roomid is error,or this room is not open!" + ); + } + const streamInfo: object = data['avp_info_res']['stream_line_addr'] + const streamUrl: any = Object.values(streamInfo)[0]['cdn_info']['url'] + resolve(streamUrl); }) .catch(function (error: any) { reject(error); diff --git a/src/type/biliAPIResponse.ts b/src/type/biliAPIResponse.ts index 94b59a9..12962fd 100644 --- a/src/type/biliAPIResponse.ts +++ b/src/type/biliAPIResponse.ts @@ -13,7 +13,8 @@ export type GetQRCodeResponse = { export type LoginResponse = { is_new: boolean, - token_info: TokenInfo + token_info: TokenInfo, + cookie_info: CookieInfo } export type GetUserInfoResponse = { @@ -26,4 +27,17 @@ export type TokenInfo = { access_token: string, refresh_token: string, expires_in: number +} + +export type CookieInfo = { + cookies: Cookie[], + domains: string[] +} + +export type Cookie = { + name: string, + value: string, + http_only: number, + expires: number, + secure: number } \ No newline at end of file diff --git a/src/type/config.ts b/src/type/config.ts index fd678b1..a8525ec 100644 --- a/src/type/config.ts +++ b/src/type/config.ts @@ -32,7 +32,8 @@ export interface PersonInfo { refresh_token: string, expires_in: number, tokenSignDate: number, - mid: number + mid: number, + cookies: string } export interface StreamerInfo { diff --git a/src/uploader/user.ts b/src/uploader/user.ts index bd88046..f8826d7 100644 --- a/src/uploader/user.ts +++ b/src/uploader/user.ts @@ -9,7 +9,7 @@ const qr2image = require('qr-image') import { $axios } from "@/http"; import { getExtendedLogger } from "@/log"; import * as crypt from '@/util/crypt' -import { BiliAPIResponse, LoginResponse, GetQRCodeResponse, GetUserInfoResponse } from "@/type/biliAPIResponse"; +import { BiliAPIResponse, LoginResponse, GetQRCodeResponse, GetUserInfoResponse, Cookie, CookieInfo } from "@/type/biliAPIResponse"; import { PersonInfo } from "@/type/config"; export class User { @@ -23,6 +23,7 @@ export class User { private _expires_in: number; private _nickname: string | undefined; private _tokenSignDate: number; + private _cookies: string; constructor(personInfo: PersonInfo) { @@ -33,6 +34,7 @@ export class User { this._tokenSignDate = personInfo.tokenSignDate; this.logger = getExtendedLogger('User') this._mid = personInfo.mid || 0; + this._cookies = personInfo.cookies; } get access_token(): string { @@ -43,6 +45,10 @@ export class User { this._access_token = value; } + get cookies(): string { + return this._cookies + } + /** * 同步个人数据到配置文件 */ @@ -58,7 +64,8 @@ export class User { refresh_token: this._refresh_token, expires_in: this._expires_in, tokenSignDate: this._tokenSignDate, - mid: this._mid + mid: this._mid, + cookies: this._cookies } const stringified = JSON.stringify(infoObj, null, ' ') fs.promises.writeFile('./templates/info.json', stringified) @@ -168,7 +175,8 @@ export class User { try { const { data: { data: { - token_info + token_info, + cookie_info }, code, message } } = await $axios.request>({ url, data: querystring.stringify(params), headers, method: "post" }) if (code === 0) { @@ -177,7 +185,7 @@ export class User { this._refresh_token = token_info.refresh_token this._expires_in = token_info.expires_in this._tokenSignDate = Date.now() - + this._buildCookies(cookie_info) this.logger.info(`Token refresh succeed !!`) resolve() } else { @@ -223,6 +231,7 @@ export class User { this._refresh_token = data.token_info.refresh_token this._mid = data.token_info.mid this._expires_in = data.token_info.expires_in + this._buildCookies(data.cookie_info) return resolve() } else { this.logger.debug(message) @@ -254,4 +263,13 @@ export class User { resolve(data) }) } + + private _buildCookies = (cookieInfo: CookieInfo) => { + const neededCookieName = ['SESSDATA', 'bili_jct', 'DedeUserID', 'DedeUserID__ckMd5'] + const cookies = cookieInfo.cookies + .filter((cookie: Cookie) => neededCookieName.includes(cookie.name)) + .map((cookie: Cookie) => `${cookie.name}=${cookie.value}`) + .join('; ') + this._cookies = cookies + } } \ No newline at end of file diff --git a/templates/info-example.json b/templates/info-example.json index 2426222..9b3bb48 100644 --- a/templates/info-example.json +++ b/templates/info-example.json @@ -27,7 +27,8 @@ "refresh_token": "", "expires_in": 0, "tokenSignDate": 0, - "mid": 0 + "mid": 0, + "cookies": "" }, "streamerInfo": [ {