feat: Add cache during runtime (#995)

* feat: Runtime modification of Links

* update

* update

* compatible with python 3.8

* update

* update

* update
This commit is contained in:
XuanRan_Nyan authored and GitHub committed 2024-07-17 01:45:39 +00:00
1 parent b1f577b2dd
commit 02f8fb5c62
19 files changed
+314 -260

No files matched your search

+17 -16
View File
@@ -9,27 +9,26 @@ body:
## 请补全标题并认真填写下方内容
请清晰描述问题,不必严格套用模板。
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: 你必须先同意以下内容
options:
- label: 我清楚知道,可以在 [Release](https://github.com/biliup/biliup/releases) 检阅最新的 Biliup 版本
- label: 我已在 [Release](https://github.com/biliup/biliup/releases) 检阅最新版本并尝试,且问题依旧
required: true
- label: 我清楚知道,反馈非最新 Release 版的错误是无意义的
required: true
- label: 我清楚知道,日志中并不包含 Cookie 此类的信息,可以直接上传完整日志
required: true
- label: 我已经尝试过最新 Release 版,且问题依旧
- label: 我清楚知道,可以通过附件功能上传完整文件
required: true
- type: input
id: system_version
attributes:
label: 运行环境
description: 如 Windows Server 2012、CentOS 8.2 等
description: 包括系统名称、系统版本、系统架构、Python版本
placeholder: Debian 12.6 x86_64 && Python 3.12.4 或 Docker tag:v0.4.71
validations:
required: true
@@ -38,6 +37,7 @@ body:
attributes:
label: biliup 版本
description: 可通过运行 `biliup --version` 查看
placeholder: v0.4.71
validations:
required: true
@@ -45,7 +45,8 @@ body:
id: url
attributes:
label: 所录制的主播网址
description: 可以在配置文件中查看
description: 因平台内直播分区策略有所不同,请准确填写出现问题的主播网址
placeholder: https://www.twitch.tv/lck
validations:
required: true
@@ -60,18 +61,18 @@ body:
- type: textarea
id: config
attributes:
label: 配置文件
description: 请**脱敏**后上传你的配置文件, 或将文件内容复制到下面模板中
label: 任务平台
description: 请复制WEBUI的任务平台内容并**脱敏**后放入文件上传, 或直接复制到下面模板中
value: |
<details>
<summary>
配置文件
任务平台
</summary>
```bash
# 在这里填写你的配置文件
```
</details>
- type: textarea
@@ -84,13 +85,13 @@ body:
<summary>
### 日志
</summary>
```bash
# 在这里填写你的报错日志
```
</details>
### 其他补充描述
<!--此处填写补充描述-->
+1 -1
View File
@@ -209,7 +209,7 @@ const Dashboard: React.FC = () => {
</div>
<Collapse keepDOM style={{ width: '100%' }}>
{/* 哔哩哔哩 */}
<Bilibili />
<Bilibili entity={entity} list={list}/>
{/* CC直播 */}
<CC />
{/* 抖音 */}
+48 -1
View File
@@ -2,7 +2,14 @@
import React from "react";
import { Form, Select, Collapse } from "@douyinfe/semi-ui";
const Bilibili: React.FC = () => {
type Props = {
entity: any;
list: any;
};
const Bilibili: React.FC<Props> = (props) => {
const entity = props.entity;
const list = props.list;
return (
<>
<Collapse.Panel header="哔哩哔哩" itemKey="bilibili">
@@ -41,6 +48,46 @@ const Bilibili: React.FC = () => {
field="bilibili_danmaku"
extraText="录制哔哩哔哩弹幕,目前不支持视频按时长分段下的弹幕文件自动分段。仅限下载插件为非 stream-gears 时生效,默认关闭。"
label="录制弹幕(bilibili_danmaku)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.bili_cookie"
extraText={
<div className="semi-form-field-extra">
根据格式填入cookie。推荐使用「
<a
href="https://github.com/biliup/biliup-rs"
title="「biliup-rs」 Github 项目主页"
target="_blank"
>
biliup-rs
</a>
」来获取。
<br />
</div>
}
placeholder="SESSDATA=none;bili_jct=none;DedeUserID__ckMd5=none;DedeUserID=none;"
label="哔哩哔哩 Cookie 文本(bili_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Select
field="user.bili_cookie_file"
label="哔哩哔哩 Cookie 文件(bili_cookie_file)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
optionList={list}
extraText="只支持「biliup-rs」生成的文件。当与上一个配置项同时存在时,将优先使用文件。"
showClear={true}
/>
<Form.Select
field="bili_protocol"
-120
View File
@@ -13,61 +13,6 @@ const Cookie: React.FC<Props> = (props) => {
return (
<>
<Collapse.Panel header="用户 Cookie" itemKey="user">
<Form.Input
field="user.bili_cookie"
extraText={
<div className="semi-form-field-extra">
根据格式填入cookie。推荐使用「
<a
href="https://github.com/biliup/biliup-rs"
title="「biliup-rs」 Github 项目主页"
target="_blank"
>
biliup-rs
</a>
」来获取。
<br />
</div>
}
placeholder="SESSDATA=none;bili_jct=none;DedeUserID__ckMd5=none;DedeUserID=none;"
label="哔哩哔哩 Cookie 文本(bili_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Select
field="user.bili_cookie_file"
label="哔哩哔哩 Cookie 文件(bili_cookie_file)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
optionList={list}
extraText="只支持「biliup-rs」生成的文件。当与上一个配置项同时存在时,将优先使用文件。"
showClear={true}
/>
<Form.Input
field="user.douyin_cookie"
extraText={
<div className="semi-form-field-extra">
如需要录制抖音 www.douyin.com/user/
类型链接,或遭到风控,请在此填入 Cookie。
<br />
需要__ac_nonce、__ac_signature、sessionid的值,请不要将所有
Cookie 填入。
</div>
}
placeholder="__ac_nonce=none;__ac_signature=none;sessionid=none;"
label="抖音 Cookie(douyin_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.kuaishou_cookie"
extraText={
@@ -85,71 +30,6 @@ const Cookie: React.FC<Props> = (props) => {
padding: 0,
}}
/>
<Form.Input
field="user.huya_cookie"
extraText={
<div className="semi-form-field-extra">
填入 Cookie 防止游客2分30秒限制。
需要 udb_biztoken, udb_uid, yyuid 的值,
请不要将所有 Cookie 填入。
</div>
}
placeholder="udb_uid=none;yyuid=none;udb_biztoken=none;"
label="虎牙 Cookie(huya_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.twitch_cookie"
extraText={
<div className="semi-form-field-extra">
【仅限Turbo会员】如录制Twitch时遇见视频流中广告过多的情况,可尝试在此填入cookie,可以大幅减少视频流中的twitch广告
<br />
该 Cookie
存在过期风险,Cookie过期后会在日志输出警告,请注意及时更换。
<br />
当 Cookie 失效,录制时将忽略
Cookie。(经作者个人测试,可保持未过期状态四个月以上)
<br />
twitch_cookie 获取方式:在浏览器中打开 twitch.tv ,
F12 调出控制台,在控制台中执行如下代码:
<br />
<code
style={{ color: "blue" }}
>{`document.cookie.split("; ").find(item => item.startsWith("auth-token="))?.split("=")[1]`}</code>
<br />
twitch_cookie&nbsp;需要在&nbsp;downloader=
&quot;ffmpeg&quot;&nbsp;时候才会生效。
</div>
}
label="Twitch Cookie(twitch_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.youtube_cookie"
extraText={
<div style={{ fontSize: "14px" }}>
使用Cookies登陆YouTube帐号,可用于下载会限、私享等未登录账号无法访问的内容。
<br />
<style></style>可以使用 Chrome 插件「Get
cookies.txt」来生成txt文件,请使用 Netscape 格式的
Cookies 文本路径。
</div>
}
label="TouTube Cookie(youtube_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.niconico-email"
extraText="与您的 Niconico 账户相关联的电子邮件或电话号码。"
+23
View File
@@ -42,6 +42,29 @@ const Douyin: React.FC = () => {
</div>
}
label="录制弹幕(douyin_danmaku)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.douyin_cookie"
extraText={
<div className="semi-form-field-extra">
如需要录制抖音 www.douyin.com/user/
类型链接,或遭到风控,请在此填入 Cookie。
<br />
需要__ac_nonce、__ac_signature、sessionid的值,请不要将所有
Cookie 填入。
</div>
}
placeholder="__ac_nonce=none;__ac_signature=none;sessionid=none;"
label="抖音 Cookie(douyin_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Select
field="douyin_protocol"
+4 -2
View File
@@ -259,7 +259,7 @@ const Global: React.FC = () => {
<Form.Select
field="lines"
label="上传线路(lines)"
extraText="b站上传线路选择,默认为自动模式,可手动切换为bda2, kodo, ws, qn, bldsa"
extraText="b站上传线路选择,默认为自动模式,可手动切换为bda, bda2, ws, qn, bldsa, tx, txa"
placeholder="AUTO(自动,默认)"
style={{ width: "100%" }}
fieldStyle={{
@@ -269,11 +269,13 @@ const Global: React.FC = () => {
showClear={true}
>
<Form.Select.Option value="AUTO">AUTO(自动,默认)</Form.Select.Option>
<Form.Select.Option value="bda">bda</Form.Select.Option>
<Form.Select.Option value="bda2">bda2</Form.Select.Option>
<Form.Select.Option value="kodo">kodo</Form.Select.Option>
<Form.Select.Option value="ws">ws</Form.Select.Option>
<Form.Select.Option value="qn">qn</Form.Select.Option>
<Form.Select.Option value="bldsa">bldsa</Form.Select.Option>
<Form.Select.Option value="tx">tx</Form.Select.Option>
<Form.Select.Option value="txa">txa</Form.Select.Option>
</Form.Select>
<Form.InputNumber
field="threads"
+8 -6
View File
@@ -64,12 +64,6 @@ const Huya: React.FC<Props> = (props) => {
alignSelf: "stretch",
padding: 0,
}}
rules={[
{
pattern: /^[A-Z]{2}(?:\d{2})?$/,
message: "请仅输入线路别称"
}
]}
showClear={true}
>
<Select.Option value="AL">直播线路3(AL)</Select.Option>
@@ -109,11 +103,19 @@ const Huya: React.FC<Props> = (props) => {
? entity["huya_imgplus"]
: true
}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Switch
field="huya_mobile_api"
extraText="使用移动端API可以绕过部分游客观看原画的时长限制"
label="虎牙使用移动端API(huya_mobile_api)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
</Collapse.Panel>
</>
+4
View File
@@ -11,6 +11,10 @@ const TwitCasting: React.FC = () => {
field="twitcasting_danmaku"
extraText="录制TwitCasting弹幕,默认关闭"
label="录制弹幕(twitcasting_danmaku)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="twitcasting_password"
+35 -1
View File
@@ -25,8 +25,42 @@ const Twitch: React.FC<Props> = (props) => {
field="twitch_disable_ads"
extraText="去除Twitch广告功能,默认开启
这个功能会导致Twitch录播分段,因为遇到广告就自动断开了,这就是去广告。若需要录播完整一整段可以关闭这个,但是关了之后就会有紫色屏幕的CommercialTime
还有一个不想视频分段的办法是去花钱开一个Turbo会员,能不看广告,然后下面的user里把twitch的cookie填上,也能不看广告,自然就不会分段了"
还有一个办法是去花钱开一个Turbo会员,然后下面的user里把twitch的cookie填上,也能去除广告"
label="去除广告(twitch_disable_ads)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="user.twitch_cookie"
extraText={
<div className="semi-form-field-extra">
【仅限Turbo会员】如录制Twitch时遇见视频流中广告过多的情况,可尝试在此填入cookie,可以大幅减少视频流中的twitch广告
<br />
该 Cookie
存在过期风险,Cookie过期后会在日志输出警告,请注意及时更换。
<br />
当 Cookie 失效,录制时将忽略
Cookie。(经作者个人测试,可保持未过期状态四个月以上)
<br />
twitch_cookie 获取方式:在浏览器中打开 twitch.tv ,
F12 调出控制台,在控制台中执行如下代码:
<br />
<code
style={{ color: "blue" }}
>{`document.cookie.split("; ").find(item => item.startsWith("auth-token="))?.split("=")[1]`}</code>
<br />
twitch_cookie&nbsp;需要在&nbsp;downloader=
&quot;ffmpeg&quot;&nbsp;时候才会生效。
</div>
}
label="Twitch Cookie(twitch_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
</Collapse.Panel>
</>
+26
View File
@@ -11,6 +11,24 @@ const YouTube: React.FC<Props> = (props) => {
return (
<>
<Collapse.Panel header="YouTube" itemKey="youtube">
<Form.Input
field="user.youtube_cookie"
extraText={
<div style={{ fontSize: "14px" }}>
使用Cookies登陆YouTube帐号,可用于下载会限、私享等未登录账号无法访问的内容。
<br />
<style></style>可以使用 Chrome 插件「Get
cookies.txt」来生成txt文件,请使用 Netscape 格式的
Cookies 文本路径。
</div>
}
label="YouTube Cookie(youtube_cookie)"
style={{ width: "100%" }}
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Switch
initValue={
entity?.hasOwnProperty("youtube_enable_download_live")
@@ -26,6 +44,10 @@ const YouTube: React.FC<Props> = (props) => {
例如录制https://www.youtube.com/@NeneAmanoCh/streams,关闭后将忽略正在直播
"
label="下载直播(youtube_enable_download_live)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Switch
initValue={
@@ -42,6 +64,10 @@ const YouTube: React.FC<Props> = (props) => {
例如录制https://www.youtube.com/@NeneAmanoCh/streams,关闭后将忽略直播回放
"
label="下载回放(youtube_enable_download_playback)"
fieldStyle={{
alignSelf: "stretch",
padding: 0,
}}
/>
<Form.Input
field="youtube_after_date"
+2 -2
View File
@@ -24,7 +24,7 @@ class Acfun(DownloadBase):
cookies = dict(_did=did)
data1 = {'sid': 'acfun.api.visitor'}
r1 = await biliup.common.util.client.post("https://id.app.acfun.cn/rest/app/visitor/login",
headers=self.fake_headers, data=data1, cookies=cookies, timeout=5)
headers=self.fake_headers, data=data1, cookies=cookies)
userid = r1.json()['userId']
visitorst = r1.json()['acfun.api.visitor_st']
params = {
@@ -38,7 +38,7 @@ class Acfun(DownloadBase):
data2 = {'authorId': rid, 'pullStreamType': 'FLV'}
self.fake_headers['referer'] = "https://live.acfun.cn/"
r2 = await biliup.common.util.client.post("https://api.kuaishouzt.com/rest/zt/live/web/startPlay",
headers=self.fake_headers, data=data2, params=params, timeout=5)
headers=self.fake_headers, data=data2, params=params)
if r2.json().get('result') != 1:
logger.debug(r2.json())
return False
+25 -26
View File
@@ -346,46 +346,45 @@ class BiliBili:
"""
preferred_upos_cdn = None
if not self._auto_os:
if lines == 'kodo':
self._auto_os = {"os": "kodo", "query": "bucket=bvcupcdnkodobm&probe_version=20221109",
"probe_url": "//up-na0.qbox.me/crossdomain.xml"}
elif lines == 'bda2':
self._auto_os = {"os": "upos", "query": "upcdn=bda2&probe_version=20221109",
"probe_url": "//upos-sz-upcdnbda2.bilivideo.com/OK"}
preferred_upos_cdn = 'bda2'
elif lines == 'cs-bda2':
if lines == 'bda':
self._auto_os = {"os": "upos", "query": "upcdn=bda&probe_version=20221109",
"probe_url": "//upos-cs-upcdnbda.bilivideo.com/OK"}
preferred_upos_cdn = 'bda'
elif lines in {'bda2', 'cs-bda2'}:
self._auto_os = {"os": "upos", "query": "upcdn=bda2&probe_version=20221109",
"probe_url": "//upos-cs-upcdnbda2.bilivideo.com/OK"}
preferred_upos_cdn = 'bda2'
elif lines == 'ws':
self._auto_os = {"os": "upos", "query": "upcdn=ws&probe_version=20221109",
"probe_url": "//upos-sz-upcdnws.bilivideo.com/OK"}
"probe_url": "//upos-cs-upcdnws.bilivideo.com/OK"}
preferred_upos_cdn = 'ws'
elif lines == 'qn':
self._auto_os = {"os": "upos", "query": "upcdn=qn&probe_version=20221109",
"probe_url": "//upos-sz-upcdnqn.bilivideo.com/OK"}
preferred_upos_cdn = 'qn'
elif lines == 'cs-qn':
elif lines in {'qn', 'cs-qn'}:
self._auto_os = {"os": "upos", "query": "upcdn=qn&probe_version=20221109",
"probe_url": "//upos-cs-upcdnqn.bilivideo.com/OK"}
preferred_upos_cdn = 'qn'
elif lines == 'cos':
self._auto_os = {"os": "cos", "query": "",
"probe_url": ""}
elif lines == 'cos-internal':
self._auto_os = {"os": "cos-internal", "query": "",
"probe_url": ""}
elif lines == 'bldsa':
self._auto_os = {"os": "upos", "query": "upcdn=bldsa&probe_version=20221109",
"probe_url": "//upos-cs-upcdnbldsa.bilivideo.com/OK"}
preferred_upos_cdn = 'bldsa'
elif lines == 'tx':
self._auto_os = {"os": "upos", "query": "upcdn=tx&probe_version=20221109",
"probe_url": "//upos-cs-upcdntx.bilivideo.com/OK"}
preferred_upos_cdn = 'tx'
elif lines == 'txa':
self._auto_os = {"os": "upos", "query": "upcdn=txa&probe_version=20221109",
"probe_url": "//upos-cs-upcdntxa.bilivideo.com/OK"}
preferred_upos_cdn = 'txa'
else:
self._auto_os = self.probe()
logger.info(f"线路选择 => {self._auto_os['os']}: {self._auto_os['query']}. time: {self._auto_os.get('cost')}")
if self._auto_os['os'] == 'upos':
upload = self.upos
elif self._auto_os['os'] == 'cos':
upload = self.cos
elif self._auto_os['os'] == 'cos-internal':
upload = lambda *args, **kwargs: self.cos(*args, **kwargs, internal=True)
elif self._auto_os['os'] == 'kodo':
upload = self.kodo
# elif self._auto_os['os'] == 'cos':
# upload = self.cos
# elif self._auto_os['os'] == 'cos-internal':
# upload = lambda *args, **kwargs: self.cos(*args, **kwargs, internal=True)
# elif self._auto_os['os'] == 'kodo':
# upload = self.kodo
else:
logger.error(f"NoSearch:{self._auto_os['os']}")
raise NotImplementedError(self._auto_os['os'])
+17 -5
View File
@@ -12,8 +12,7 @@ from ..engine.download import DownloadBase
OFFICIAL_API = "https://api.live.bilibili.com"
@Plugin.download(regexp=r'(?:https?://)?live\.bilibili\.com')
@Plugin.download(regexp=r'(?:https?://)?b23\.tv')
@Plugin.download(regexp=r'(?:https?://)?(b23\.tv|live\.bilibili\.com)')
class Bililive(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
@@ -22,7 +21,7 @@ class Bililive(DownloadBase):
self.fake_headers['referer'] = url
if config.get('user', {}).get('bili_cookie'):
self.fake_headers['cookie'] = config.get('user', {}).get('bili_cookie')
elif config.get('user', {}).get('bili_cookie_file'):
if config.get('user', {}).get('bili_cookie_file'):
cookie_file_name = config.get('user', {}).get('bili_cookie_file')
try:
with open(cookie_file_name, encoding='utf-8') as stream:
@@ -37,10 +36,23 @@ class Bililive(DownloadBase):
# logger.warning("No cookie provided. The original quality may not be available.")
async def acheck_stream(self, is_check=False):
room_id = match1(self.url, r'/(\d+)')
qualityNumber = int(config.get('bili_qn', 10000))
client.headers.update(self.fake_headers)
if "b23.tv" in self.url:
try:
url = (await client.get(self.url, follow_redirects=False)).next_request.url
if "live.bilibili" in str(url):
self.url = url
else:
raise
except:
logger.error(f"{self.plugin_msg}: 不支持的链接")
return False
room_id = match1(self.url, r'bilibili.com/(\d+)')
qualityNumber = int(config.get('bili_qn', 10000))
# 获取直播状态与房间标题
info_by_room_url = f"{OFFICIAL_API}/xlive/web-room/v1/index/getInfoByRoom?room_id={room_id}"
try:
+6 -6
View File
@@ -118,18 +118,18 @@ class BiliWeb(UploadBase):
def stream_gears_upload(ex_conn, lines, *args, **kwargs):
try:
if lines == 'kodo':
kwargs['line'] = stream_gears.UploadLine.Kodo
if lines == 'bda':
kwargs['line'] = stream_gears.UploadLine.Bda
elif lines == 'bda2':
kwargs['line'] = stream_gears.UploadLine.Bda2
elif lines == 'ws':
kwargs['line'] = stream_gears.UploadLine.Ws
elif lines == 'qn':
kwargs['line'] = stream_gears.UploadLine.Qn
elif lines == 'cos':
kwargs['line'] = stream_gears.UploadLine.Cos
elif lines == 'cos-internal':
kwargs['line'] = stream_gears.UploadLine.CosInternal
elif lines == 'tx':
kwargs['line'] = stream_gears.UploadLine.Tx
elif lines == 'txa':
kwargs['line'] = stream_gears.UploadLine.Txa
elif lines == 'bldsa':
kwargs['line'] = stream_gears.UploadLine.Bldsa
+30 -27
View File
@@ -1,19 +1,21 @@
import hashlib
import time
from urllib.parse import parse_qs
from functools import lru_cache
from biliup.common.util import client
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
from ..plugins import logger, match1, random_user_agent
@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m)\.)?douyu\.com')
class Douyu(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
self._room_id = match1(url, r'rid=(\d+)')
self.douyu_danmaku = config.get('douyu_danmaku', False)
async def acheck_stream(self, is_check=False):
@@ -22,24 +24,15 @@ class Douyu(DownloadBase):
return False
try:
room_id = self.url.split('douyu.com/')[1].split('/')[0].split('?')[0]
# 暂时只判断纯数字的room_id
if not room_id.isdigit() or len(room_id) < 6:
room_id = 0
if room_id == 0:
resp = await client.get(self.url, headers=self.fake_headers, timeout=5)
room_id = match1(resp.text, r'\$ROOM\.room_id\s*=\s*(\d+)', r'apm_room_id\s*=\s*(\d+)')[0]
if not room_id:
logger.error(f"{self.plugin_msg}: 直播间不存在或已关闭")
return False
if not self._room_id:
self._room_id = _get_real_rid(self.url)
except:
logger.exception(f"{self.plugin_msg}: 获取房间号错误")
return False
try:
room_info = (
await client.get(f"https://www.douyu.com/betard/{room_id}",
headers=self.fake_headers, timeout=5)
await client.get(f"https://www.douyu.com/betard/{self._room_id}", headers=self.fake_headers)
).json()['room']
except:
logger.exception(f"{self.plugin_msg}: 获取直播间信息错误")
@@ -52,8 +45,10 @@ class Douyu(DownloadBase):
logger.debug(f"{self.plugin_msg}: 正在放录播")
return False
if config.get('douyu_disable_interactive_game', False):
gift_info = (await client.get(f"https://www.douyu.com/api/interactive/web/v2/list?rid={room_id}",
headers=self.fake_headers, timeout=5)).json().get('data', {})
gift_info = (
await client.get(f"https://www.douyu.com/api/interactive/web/v2/list?rid={self._room_id}",
headers=self.fake_headers)
).json().get('data', {})
if gift_info:
logger.debug(f"{self.plugin_msg}: 正在运行互动游戏")
return False
@@ -77,18 +72,18 @@ class Douyu(DownloadBase):
import jsengine
ctx = jsengine.jsengine()
js_enc = (
await client.get(f'https://www.douyu.com/swf_api/homeH5Enc?rids={room_id}',
headers=self.fake_headers, timeout=5)
).json()['data'][f'room{room_id}']
await client.get(f'https://www.douyu.com/swf_api/homeH5Enc?rids={self._room_id}',
headers=self.fake_headers)
).json()['data'][f'room{self._room_id}']
js_enc = js_enc.replace('return eval', 'return [strc, vdwdae325w_64we];')
sign_fun, sign_v = ctx.eval(f'{js_enc};ub98484234();')
tt = str(int(time.time()))
did = hashlib.md5(tt.encode('utf-8')).hexdigest()
rb = hashlib.md5(f"{room_id}{did}{tt}{sign_v}".encode('utf-8')).hexdigest()
rb = hashlib.md5(f"{self._room_id}{did}{tt}{sign_v}".encode('utf-8')).hexdigest()
sign_fun = sign_fun.rstrip(';').replace("CryptoJS.MD5(cb).toString()", f'"{rb}"')
sign_fun += f'("{room_id}","{did}","{tt}");'
sign_fun += f'("{self._room_id}","{did}","{tt}");'
params = parse_qs(ctx.eval(sign_fun))
except:
@@ -100,7 +95,7 @@ class Douyu(DownloadBase):
params['rate'] = config.get('douyu_rate', 0)
try:
live_data = await self.get_play_info(room_id, params)
live_data = await self.get_play_info(self._room_id, params)
self.raw_stream_url = f"{live_data['rtmp_url']}/{live_data['rtmp_live']}"
except:
logger.exception(f"{self.plugin_msg}: ")
@@ -113,17 +108,25 @@ class Douyu(DownloadBase):
self.danmaku = DanmakuClient(self.url, self.gen_download_filename())
async def get_play_info(self, room_id, params):
__cdn_check = lambda _name, _list: any(_name in _item['cdn'] for _item in _list)
live_data = await client.post(f'https://www.douyu.com/lapi/live/getH5Play/{room_id}',
headers=self.fake_headers, params=params, timeout=5)
live_data = await client.post(
f'https://www.douyu.com/lapi/live/getH5Play/{room_id}', headers=self.fake_headers, params=params)
if not live_data.is_success:
raise RuntimeError(live_data.text)
live_data = live_data.json().get('data')
if isinstance(live_data, dict):
if not live_data['rtmp_cdn'].endswith('h5') or 'akm' in live_data['rtmp_cdn']:
params['cdn'] = 'tct-h5' if __cdn_check('tct-h5', live_data['cdnsWithName']) \
else live_data['cdnsWithName'][-1]['cdn']
params['cdn'] = live_data['cdnsWithName'][-1]['cdn']
return await self.get_play_info(room_id, params)
return live_data
raise RuntimeError(live_data)
@lru_cache(maxsize=None)
def _get_real_rid(url):
import requests
headers = {
"user-agent": random_user_agent('mobile'),
}
rid = url.split('douyu.com/')[1].split('/')[0].split('?')[0] or match1(url, r'douyu.com/(\d+)')
resp = requests.get(f"https://m.douyu.com/{rid}", headers=headers)
real_rid = match1(resp.text, r'roomInfo":{"rid":(\d+)')
return real_rid
+58 -37
View File
@@ -4,40 +4,37 @@ import json
import random
import time
from urllib.parse import parse_qs, unquote
from functools import lru_cache
from biliup.common.util import client
from biliup.config import config
from biliup.Danmaku import DanmakuClient
from ..engine.decorators import Plugin
from ..engine.download import DownloadBase
from ..plugins import logger
from ..plugins import logger, random_user_agent
@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m)\.)?huya\.com')
class Huya(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
self.huya_danmaku = config.get('huya_danmaku', False)
self.fake_headers['referer'] = url
self.fake_headers['cookie'] = config.get('user', {}).get('huya_cookie', '')
self.__room_id = None
self._room_id = url.split('huya.com/')[1].split('?')[0]
self.huya_danmaku = config.get('huya_danmaku', False)
async def acheck_stream(self, is_check=False):
try:
await self.__verify_cookie()
self.__room_id = self.url.split('huya.com/')[1]
if not self.__room_id:
raise
if not self.__room_id.isdigit():
html_info = await self.get_room_profile()
if not html_info:
raise
self.__room_id = html_info['data'][0]['gameLiveInfo']['profileRoom']
if self.fake_headers.get('cookie'):
await self.verify_cookie()
if not self._room_id.isdigit():
self._room_id = _get_real_rid(self.url)
room_profile = await self.get_room_profile(use_api=True)
except Exception as e:
logger.error(f"{self.plugin_msg}: {e}")
return False
room_profile = await self.get_room_profile(use_api=True)
if room_profile['realLiveStatus'] != 'ON' or room_profile['liveStatus'] != 'ON':
'''
ON: 直播
@@ -48,6 +45,11 @@ class Huya(DownloadBase):
self.raw_stream_url = None
return False
if not room_profile['liveData'].get('bitRateInfo'):
# 主播未推流
logger.debug(f"{self.plugin_msg} : 未推流")
return False
if is_check:
return True
@@ -117,6 +119,7 @@ class Huya(DownloadBase):
self.raw_stream_url += f"&ratio={record_ratio}"
return True
def danmaku_init(self):
if self.huya_danmaku:
self.danmaku = DanmakuClient(self.url, self.gen_download_filename())
@@ -124,17 +127,18 @@ class Huya(DownloadBase):
async def get_room_profile(self, use_api=False) -> dict:
if use_api:
resp = (await client.get(f"https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid={self.__room_id}", \
headers=self.fake_headers)).json()
resp = (await client.get(f"https://mp.huya.com/cache.php?m=Live&do=profileRoom&roomid={self._room_id}", \
headers=self.fake_headers)).json()
if resp['status'] != 200:
raise Exception(f"{self.plugin_msg}: {resp['message']}")
raise Exception(f"{resp['message']}")
return resp['data']
else:
html = (await client.get(f"https://www.huya.com/{self.__room_id}", headers=self.fake_headers)).text
html = (await client.get(f"https://www.huya.com/{self._room_id}", headers=self.fake_headers)).text
if '找不到这个主播' in html:
raise Exception(f"{self.plugin_msg}: 找不到这个主播")
raise Exception(f"找不到这个主播")
return json.loads(html.split('stream: ')[1].split('};')[0])
async def get_stream_urls(self, protocol, use_api=False, allow_imgplus=True) -> dict:
'''
返回指定协议的所有CDN流
@@ -145,25 +149,27 @@ class Huya(DownloadBase):
try:
stream_info = room_profile['data'][0]['gameStreamInfoList']
except KeyError:
raise Exception(f"{self.plugin_msg}: {room_profile}")
raise Exception(f"{room_profile}")
else:
stream_info = room_profile['stream']['baseSteamInfoList']
streams = self.__dict_sorting(json.loads(room_profile['liveData']['mStreamRatioWeb']))
streams = _dict_sorting(json.loads(room_profile['liveData'].get('mStreamRatioWeb', '{}')))
stream = stream_info[0]
stream_name = stream['sStreamName']
suffix, anti_code = stream[f's{protocol}UrlSuffix'], stream[f's{protocol}AntiCode']
if not allow_imgplus:
stream_name = stream_name.replace('-imgplus', '')
anti_code = self.__build_query(stream_name, anti_code, )
anti_code = self.__build_query(stream_name, anti_code, self.fake_headers['cookie'])
for stream in stream_info:
stream_url = f"{stream[f's{protocol}Url']}/{stream_name}.{suffix}?{anti_code}"
if stream['sCdnType'] in ['HY', 'HUYA', 'HYZJ']: continue
streams[stream['sCdnType']] = f"{stream[f's{protocol}Url']}/{stream_name}.{suffix}?{anti_code}"
streams[stream['sCdnType']] = stream_url
return streams
def __build_query(self, stream_name, anti_code) -> str:
@staticmethod
def __build_query(stream_name, anti_code, cookies=None) -> str:
url_query = parse_qs(anti_code)
platform_id = 100
uid = self.__get_uid(self.fake_headers['cookie'], stream_name)
uid = _get_uid(cookies, stream_name)
convert_uid = (uid << 8 | uid >> (32 - 8)) & 0xFFFFFFFF
ws_time = url_query['wsTime'][0]
ct = int((int(ws_time, 16) + random.random()) * 1000)
@@ -197,7 +203,8 @@ class Huya(DownloadBase):
}
return '&'.join([f"{k}={v}" for k, v in anti_code.items()])
async def __verify_cookie(self):
async def verify_cookie(self):
if self.fake_headers['cookie']:
resp = (await client.post('https://udblgn.huya.com/web/cookie/verify', \
headers=self.fake_headers, data={'appId': 5002})).json()
@@ -205,18 +212,32 @@ class Huya(DownloadBase):
logger.error(f"{self.plugin_msg}: {resp.json()['message']}")
self.fake_headers['cookie'] = ''
@staticmethod
def __dict_sorting(data: dict) -> dict:
@lru_cache(maxsize=None)
def _get_real_rid(url):
import requests
headers = {
'user-agent': random_user_agent(),
}
html = requests.get(url, headers=headers).text
if '找不到这个主播' in html:
raise Exception(f"找不到这个主播")
html_obj = json.loads(html.split('stream: ')[1].split('};')[0])
return html_obj['data'][0]['gameLiveInfo']['profileRoom']
def _dict_sorting(data: dict) -> dict:
if data:
data = {k: v for k, v in data.items() if k not in ['HY', 'HUYA', 'HYZJ']}
return dict(sorted(data.items(), key=lambda x: x[1], reverse=True))
return {}
@staticmethod
def __get_uid(cookie: str, stream_name: str) -> int:
if cookie:
cookie_dict = {k.strip(): v for k, v in (item.split('=') for item in cookie.split(';'))}
for key in ['udb_uid', 'yyuid']:
if key in cookie_dict:
return int(cookie_dict[key])
if stream_name:
return int(stream_name.split('-')[0])
return random.randint(1400000000000, 1499999999999)
def _get_uid(cookie: str, stream_name: str) -> int:
if cookie:
cookie_dict = {k.strip(): v for k, v in (item.split('=') for item in cookie.split(';'))}
for key in ['udb_uid', 'yyuid']:
if key in cookie_dict:
return int(cookie_dict[key])
if stream_name:
return int(stream_name.split('-')[0])
return random.randint(1400000000000, 1499999999999)
@@ -72,6 +72,7 @@ def check_access(
if hashed_password != hashed_request_password:
return False
# log.debug('%r log in successed', "biliup")
return True
+3 -3
View File
@@ -3,6 +3,7 @@ HTTP Basic Auth python lib from https://github.com/bugov/http-basic-auth
"""
import base64
from typing import Tuple
__version__ = '1.2.0'
@@ -11,8 +12,7 @@ class BasicAuthException(Exception):
"""General exception for all http-basic-auth problems
"""
# FIXME: 应使用 aes-128-gcm
def parse_token(token: str, coding='utf-8') -> tuple[str, str]:
def parse_token(token: str, coding='utf-8') -> Tuple[str, str]:
"""Get login + password tuple from Basic Auth token.
"""
try:
@@ -57,7 +57,7 @@ def generate_token(login: str, password: str, coding='utf-8') -> str:
return str(b_token, encoding=coding)
def parse_header(header_value: str, coding='utf-8') -> tuple[str, str]:
def parse_header(header_value: str, coding='utf-8') -> Tuple[str, str]:
"""Get login + password tuple from Basic Auth header value.
"""
if header_value is None:
+6 -7
View File
@@ -14,7 +14,7 @@ dependencies = [
"stream-gears >= 0.2.0,<0.3",
"psutil >= 5.4.6",
"aiofiles >= 22.1.0",
"yt-dlp >= 2024.3.10",
"yt-dlp >= 2024.7.9",
"Pillow >= 5.2.0",
"aiohttp[speedups]",
"aiohttp-cors",
@@ -22,13 +22,12 @@ dependencies = [
"httpx[http2] >= 0.27.0",
"httpcore > 1.0.3",
"PyYAML >= 4.2b1",
"streamlink >= 6.5.0",
"ykdl == 1.8.0; python_version < '3.8'",
"ykdl >= 1.8.0; python_version >= '3.8'",
"streamlink >= 6.8.3",
"ykdl >= 1.8.0",
"rsa >= 4.6",
'importlib-resources; python_version<"3.9"',
'tomli >= 1.1.0 ; python_version < "3.11"',
'protobuf >= 4.23.0',
"importlib-resources; python_version < '3.9'",
"tomli >= 1.1.0 ; python_version < '3.11'",
"protobuf >= 4.23.0",
"SQLAlchemy>=2.0.0",
"alembic>=1.12.1",
"m3u8",