feat(douyu): 构建斗鱼直播 tct & hs 流链接 (#1256)
* feat(douyu): 构建斗鱼直播流链接 * 构建斗鱼直播流链接 * fix func err * cleanup * 默认 cdn 更换为 hw-h5 * Update __init__.py --------- Co-authored-by: ForgQi <34411314+ForgQi@users.noreply.github.com>
This commit is contained in:
1 parent
32162101f1
commit
0b00a33f92
3 files changed
+173
-19
No files matched your search
+1
-1
@@ -2,7 +2,7 @@ import logging
|
||||
import platform
|
||||
import sys
|
||||
|
||||
__version__ = "0.4.93"
|
||||
__version__ = "0.4.94"
|
||||
|
||||
LOG_CONF = {
|
||||
'version': 1,
|
||||
|
||||
@@ -210,7 +210,10 @@ class DownloadBase(ABC):
|
||||
# 文件名不含后戳
|
||||
fmt_file_name = self.gen_download_filename(is_fmt=True)
|
||||
# ffmpeg 输入参数
|
||||
input_args = []
|
||||
input_args = [
|
||||
# '-http_proxy', 'http://127.0.0.1:10808',
|
||||
# "-fflags", "+genpts",
|
||||
]
|
||||
# ffmpeg 输出参数
|
||||
output_args = [
|
||||
'-c',
|
||||
|
||||
+168
-17
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import time
|
||||
from urllib.parse import parse_qs
|
||||
import random
|
||||
from urllib.parse import parse_qs, urlencode, quote
|
||||
from functools import lru_cache
|
||||
|
||||
from biliup.common.util import client
|
||||
@@ -8,7 +9,7 @@ from biliup.config import config
|
||||
from biliup.Danmaku import DanmakuClient
|
||||
from ..engine.decorators import Plugin
|
||||
from ..engine.download import DownloadBase
|
||||
from ..plugins import logger, match1, random_user_agent
|
||||
from ..plugins import logger, match1, random_user_agent, json_loads
|
||||
|
||||
|
||||
@Plugin.download(regexp=r'https?://(?:(?:www|m)\.)?douyu\.com')
|
||||
@@ -18,7 +19,7 @@ class Douyu(DownloadBase):
|
||||
self.__room_id = match1(url, r'rid=(\d+)')
|
||||
self.douyu_danmaku = config.get('douyu_danmaku', False)
|
||||
self.douyu_disable_interactive_game = config.get('douyu_disable_interactive_game', False)
|
||||
self.douyu_cdn = config.get('douyu_cdn', 'tct-h5')
|
||||
self.douyu_cdn = config.get('douyu_cdn', 'hw-h5')
|
||||
self.douyu_rate = config.get('douyu_rate', 0)
|
||||
|
||||
async def acheck_stream(self, is_check=False):
|
||||
@@ -26,6 +27,9 @@ class Douyu(DownloadBase):
|
||||
logger.error(f"{self.plugin_msg}: 直播间地址错误")
|
||||
return False
|
||||
|
||||
self.fake_headers['referer'] = f"https://www.douyu.com"
|
||||
self.fake_headers['origin'] = f"https://www.douyu.com"
|
||||
|
||||
try:
|
||||
if not self.__room_id:
|
||||
self.__room_id = _get_real_rid(self.url)
|
||||
@@ -34,9 +38,9 @@ class Douyu(DownloadBase):
|
||||
return False
|
||||
|
||||
try:
|
||||
room_info = (
|
||||
await client.get(f"https://www.douyu.com/betard/{self.__room_id}", headers=self.fake_headers)
|
||||
).json()['room']
|
||||
room_info = await client.get(f"https://www.douyu.com/betard/{self.__room_id}", headers=self.fake_headers)
|
||||
room_info.raise_for_status()
|
||||
room_info = json_loads(room_info.text)['room']
|
||||
except:
|
||||
logger.exception(f"{self.plugin_msg}: 获取直播间信息错误")
|
||||
return False
|
||||
@@ -93,16 +97,43 @@ class Douyu(DownloadBase):
|
||||
logger.exception(f"{self.plugin_msg}: 获取签名参数异常")
|
||||
return False
|
||||
|
||||
params['cdn'] = self.douyu_cdn
|
||||
params['rate'] = int(self.douyu_rate)
|
||||
params.update({
|
||||
'cdn': [self.douyu_cdn],
|
||||
'rate': [str(self.douyu_rate)],
|
||||
'iar': ['1'],
|
||||
'ive': ['0'],
|
||||
'rid': self.__room_id,
|
||||
'hevc': ['0'],
|
||||
'fa': ['0'],
|
||||
'sov': ['0'],
|
||||
'ver': ['219032101'],
|
||||
})
|
||||
|
||||
self._req_data = parse_qs(urlencode(params, doseq=True, encoding='utf-8'))
|
||||
|
||||
try:
|
||||
live_data = await self.get_play_info(self.__room_id, params)
|
||||
self.raw_stream_url = f"{live_data['rtmp_url']}/{live_data['rtmp_live']}"
|
||||
live_data = await self.get_play_info(self.__room_id, self._req_data)
|
||||
url = f"{live_data['rtmp_url']}/{live_data['rtmp_live']}"
|
||||
except:
|
||||
logger.exception(f"{self.plugin_msg}: ")
|
||||
return False
|
||||
|
||||
host = None
|
||||
is_tct = (live_data['rtmp_url'].find('tc-tct.douyucdn2.cn') != -1)
|
||||
# HACK: 构建斗鱼直播流链接
|
||||
try:
|
||||
if self.douyu_cdn == 'hs-h5':
|
||||
host, _, url = await self.build_hs_url(url)
|
||||
elif self.douyu_cdn == 'tct-h5' and not is_tct:
|
||||
_, _, url = await self.build_tx_url(url)
|
||||
except (RuntimeError, ValueError) as e:
|
||||
logger.error(f"{self.plugin_msg}: {e}")
|
||||
|
||||
if url:
|
||||
self.raw_stream_url = url
|
||||
if host:
|
||||
self.fake_headers['host'] = host
|
||||
|
||||
return True
|
||||
|
||||
def danmaku_init(self):
|
||||
@@ -112,19 +143,139 @@ class Douyu(DownloadBase):
|
||||
}
|
||||
self.danmaku = DanmakuClient(self.url, self.gen_download_filename(), content)
|
||||
|
||||
async def get_play_info(self, room_id, params):
|
||||
live_data = await client.post(
|
||||
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', headers=self.fake_headers, params=params)
|
||||
async def get_play_info(self, room_id, data, mobile=False, preview=False):
|
||||
req_headers = self.fake_headers.copy()
|
||||
if mobile:
|
||||
url = 'https://m.douyu.com/api/room/ratestream'
|
||||
elif preview:
|
||||
c_time_str = str(time.time_ns())
|
||||
url = f'https://playweb.douyucdn.cn/lapi/live/hlsH5Preview/{room_id}?{c_time_str[:18]}'
|
||||
data = {
|
||||
'rid': self.__room_id,
|
||||
'did': data.get('did', ["10000000000000000000000000001501"])[0],
|
||||
}
|
||||
req_headers.update({
|
||||
'Rid': self.__room_id,
|
||||
'Time': c_time_str[:13],
|
||||
'Auth': hashlib.md5(f"{self.__room_id}{c_time_str[:13]}".encode('utf-8')).hexdigest(),
|
||||
})
|
||||
else:
|
||||
url = f'https://www.douyu.com/lapi/live/getH5Play/{room_id}'
|
||||
live_data = await client.post(url, headers=req_headers, data=data)
|
||||
if not live_data.is_success:
|
||||
raise RuntimeError(live_data.text)
|
||||
live_data = live_data.json().get('data')
|
||||
live_data = json_loads(live_data.text).get('data')
|
||||
if mobile or preview:
|
||||
return live_data
|
||||
if isinstance(live_data, dict):
|
||||
if not live_data['rtmp_cdn'].endswith('h5') or 'akm' in live_data['rtmp_cdn']:
|
||||
params['cdn'] = live_data['cdnsWithName'][-1]['cdn']
|
||||
return await self.get_play_info(room_id, params)
|
||||
if live_data.get('rtmp_cdn', '').startswith('scdn'):
|
||||
import copy
|
||||
__data = copy.deepcopy(data)
|
||||
__data['cdn'] = live_data['cdnsWithName'][-1]['cdn']
|
||||
return await self.get_play_info(room_id, __data)
|
||||
return live_data
|
||||
raise RuntimeError(live_data)
|
||||
|
||||
def get_p2p_play_info(self, xp2p_unity_api_domain):
|
||||
url = f"https://{xp2p_unity_api_domain}"
|
||||
pass
|
||||
|
||||
|
||||
def parse_stream_info(self, url) -> tuple[str, str, str]:
|
||||
'''
|
||||
解析推流信息
|
||||
'''
|
||||
def get_tx_app_name(rtmp_url) -> str:
|
||||
'''
|
||||
获取腾讯云推流应用名
|
||||
'''
|
||||
host = rtmp_url.split('//')[1].split('/')[0]
|
||||
app_name = rtmp_url.split('/')[-1]
|
||||
# group 按顺序排序
|
||||
i = match1(host, r'.+(sa|3a|1a|3|1)')
|
||||
if i:
|
||||
if i == "sa":
|
||||
i = "1"
|
||||
return f"dyliveflv{i}"
|
||||
return app_name
|
||||
list = url.split('?')
|
||||
query = parse_qs(list[1])
|
||||
origin = query.get('origin', ['0'])[0]
|
||||
if origin not in ['tct', 'hw', 'dy']:
|
||||
'''
|
||||
dy: 斗鱼自建
|
||||
tct: 腾讯云
|
||||
hw: 华为云
|
||||
'''
|
||||
raise ValueError(f"当前流来源 {origin} 不支持切换为腾讯云推流")
|
||||
elif origin == 'dy':
|
||||
logger.warning(f"{self.plugin_msg}: 当前流来源 {origin} 可能不存在腾讯云流")
|
||||
query_str = urlencode(query, doseq=True, encoding='utf-8')
|
||||
stream_id = list[0].split('/')[-1].split('.')[0]
|
||||
rtmp_url = list[0].split(stream_id)[0]
|
||||
return get_tx_app_name(rtmp_url[:-1]), stream_id.split('_')[0], query_str
|
||||
|
||||
async def build_tx_url(self, url, callback=None) -> tuple[str, str, str]:
|
||||
'''
|
||||
构建腾讯CDN URL
|
||||
return: tx_host, stream_id, tx_url
|
||||
'''
|
||||
tx_app_name, stream_id, query = self.parse_stream_info(url)
|
||||
tx_host = "tc-tct.douyucdn2.cn"
|
||||
tx_url = f"https://{tx_host}/{tx_app_name}/{stream_id}.flv?%s"
|
||||
query = parse_qs(query)
|
||||
m_data = await self.get_play_info(self.__room_id, self._req_data, mobile=True)
|
||||
_, _, m_query = self.parse_stream_info(m_data['url'])
|
||||
m_query = parse_qs(m_query)
|
||||
m_query.pop('vhost', None)
|
||||
query.update({
|
||||
'fcdn': ['tct'],
|
||||
**m_query,
|
||||
})
|
||||
query = urlencode(query, doseq=True, encoding='utf-8')
|
||||
return tx_host, stream_id, tx_url % query
|
||||
|
||||
async def build_hs_url(self, url, *, tx_host=None, stream_id=None) -> tuple[str, str, str]:
|
||||
'''
|
||||
构建火山CDN URL
|
||||
return: hs_host, stream_id, hs_ip_url
|
||||
'''
|
||||
if not tx_host:
|
||||
tx_host, stream_id, tx_url = await self.build_tx_url(url)
|
||||
else:
|
||||
tx_url = url
|
||||
query = parse_qs(tx_url.split('?')[-1])
|
||||
encoded_url = quote(tx_url, safe='')
|
||||
query.update(
|
||||
{
|
||||
'fp_user_url': [encoded_url],
|
||||
'vhost': [tx_host],
|
||||
'domain': [tx_host],
|
||||
}
|
||||
)
|
||||
query = urlencode(query, doseq=True, encoding='utf-8')
|
||||
hs_host = "douyu-pull.s.volcfcdndvs.com"
|
||||
# hs_host = "huos3.douyucdn2.cn"
|
||||
hs_ip_list = [
|
||||
"98.98.121.44", # singapore
|
||||
"128.14.110.71", # singapore
|
||||
"128.14.110.70", # singapore
|
||||
"104.166.175.26", # russia
|
||||
"104.166.175.2", # russia
|
||||
"45.43.34.227", # hongkong
|
||||
"192.169.99.102", # hongkong
|
||||
"156.59.27.66", # japan
|
||||
"156.59.27.65", # japan
|
||||
"23.248.183.107", # south-africa
|
||||
"23.248.183.106", # south-africa
|
||||
]
|
||||
hs_ip = random.choice(hs_ip_list)
|
||||
logger.info(f"{self.plugin_msg}: 使用 {hs_ip} 作为火山CDN节点")
|
||||
hs_url = f"http://{hs_host}/live/{stream_id}.flv?{query}"
|
||||
hs_ip_url = hs_url.replace(hs_host, hs_ip)
|
||||
return hs_host, stream_id, hs_ip_url
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _get_real_rid(url):
|
||||
import requests
|
||||
|
||||
Reference in new issue
Block a user