Compare commits
4 Commits
v2.0.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f16f2c4a | ||
|
|
340972c878 | ||
|
|
228e5ad46d | ||
|
|
f75d91e7f5 |
2
.github/workflows/docker-hub.yml
vendored
2
.github/workflows/docker-hub.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
||||
2
.github/workflows/ghcr.yml
vendored
2
.github/workflows/ghcr.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 更新日志
|
||||
|
||||
## 2.0.0-beta.5
|
||||
|
||||
- 兼容长 room id
|
||||
- 更新 web api
|
||||
|
||||
## 2.0.0-beta.4
|
||||
|
||||
- 添加只使用 ipv4 的命令行选项
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
__prog__ = 'blrec'
|
||||
__version__ = '2.0.0-beta.4'
|
||||
__version__ = '2.0.0-beta.5'
|
||||
__github__ = 'https://github.com/acgnhiki/blrec'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import time
|
||||
from abc import ABC
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Final, List, Mapping, Optional
|
||||
@@ -10,6 +11,7 @@ from loguru import logger
|
||||
from tenacity import retry, stop_after_delay, wait_exponential
|
||||
|
||||
from .exceptions import ApiRequestError
|
||||
from . import wbi
|
||||
from .typing import JsonResponse, QualityNumber, ResponseData
|
||||
|
||||
__all__ = 'AppApi', 'WebApi'
|
||||
@@ -23,7 +25,7 @@ BASE_HEADERS: Final = {
|
||||
'Connection': 'keep-alive',
|
||||
'Origin': 'https://live.bilibili.com',
|
||||
'Pragma': 'no-cache',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', # noqa
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36', # noqa
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +244,30 @@ class AppApi(BaseApi):
|
||||
|
||||
|
||||
class WebApi(BaseApi):
|
||||
_wbi_key = wbi.make_key(
|
||||
img_key="7cd084941338484aae1ad9425b84077c",
|
||||
sub_key="4932caff0ff746eab6f01bf08b70ac45",
|
||||
)
|
||||
_wbi_key_mtime = 0.0
|
||||
|
||||
@retry(reraise=True, stop=stop_after_delay(20), wait=wait_exponential(0.1))
|
||||
async def _get_json_res(
|
||||
self, url: str, with_wbi: bool = False, *args: Any, **kwds: Any
|
||||
) -> JsonResponse:
|
||||
if with_wbi:
|
||||
key = self.__class__._wbi_key
|
||||
ts = int(datetime.now().timestamp())
|
||||
params = list(kwds.pop("params").items())
|
||||
query = wbi.build_query(key, ts, params)
|
||||
url = f'{url}?{query}'
|
||||
|
||||
try:
|
||||
return await super()._get_json_res(url, *args, **kwds)
|
||||
except ApiRequestError as e:
|
||||
if e.code == -352 and time.monotonic() - self.__class__._wbi_key_mtime > 60:
|
||||
await self._update_wbi_key()
|
||||
raise
|
||||
|
||||
async def room_init(self, room_id: int) -> ResponseData:
|
||||
path = '/room/v1/Room/room_init'
|
||||
params = {'id': room_id}
|
||||
@@ -262,14 +288,16 @@ class WebApi(BaseApi):
|
||||
'ptype': 8,
|
||||
}
|
||||
json_responses = await self._get_jsons_concurrently(
|
||||
self.base_play_info_api_urls, path, params=params
|
||||
self.base_play_info_api_urls, path, with_wbi=True, params=params
|
||||
)
|
||||
return [r['data'] for r in json_responses]
|
||||
|
||||
async def get_info_by_room(self, room_id: int) -> ResponseData:
|
||||
path = '/xlive/web-room/v1/index/getInfoByRoom'
|
||||
params = {'room_id': room_id}
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
json_res = await self._get_json(
|
||||
self.base_live_api_urls, path, with_wbi=True, params=params
|
||||
)
|
||||
return json_res['data']
|
||||
|
||||
async def get_info(self, room_id: int) -> ResponseData:
|
||||
@@ -285,19 +313,29 @@ class WebApi(BaseApi):
|
||||
return json_res['data']['timestamp']
|
||||
|
||||
async def get_user_info(self, uid: int) -> ResponseData:
|
||||
# FIXME: "code": -352, "message": "风控校验失败",
|
||||
path = '/x/space/wbi/acc/info'
|
||||
params = {'mid': uid}
|
||||
json_res = await self._get_json(self.base_api_urls, path, params=params)
|
||||
json_res = await self._get_json(
|
||||
self.base_api_urls, path, with_wbi=True, params=params
|
||||
)
|
||||
return json_res['data']
|
||||
|
||||
async def get_danmu_info(self, room_id: int) -> ResponseData:
|
||||
path = '/xlive/web-room/v1/index/getDanmuInfo'
|
||||
params = {'id': room_id}
|
||||
json_res = await self._get_json(self.base_live_api_urls, path, params=params)
|
||||
json_res = await self._get_json(
|
||||
self.base_live_api_urls, path, with_wbi=True, params=params
|
||||
)
|
||||
return json_res['data']
|
||||
|
||||
async def get_nav(self) -> ResponseData:
|
||||
path = '/x/web-interface/nav'
|
||||
json_res = await self._get_json(self.base_api_urls, path, check_response=False)
|
||||
return json_res
|
||||
|
||||
async def _update_wbi_key(self) -> None:
|
||||
nav = await self.get_nav()
|
||||
img_key = wbi.extract_key(nav['data']['wbi_img']['img_url'])
|
||||
sub_key = wbi.extract_key(nav['data']['wbi_img']['sub_url'])
|
||||
self.__class__._wbi_key = wbi.make_key(img_key, sub_key)
|
||||
self.__class__._wbi_key_mtime = time.monotonic()
|
||||
|
||||
84
src/blrec/bili/wbi.py
Normal file
84
src/blrec/bili/wbi.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import hashlib
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
|
||||
def extract_key(url: str) -> str:
|
||||
return url.rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
||||
|
||||
|
||||
def make_key(img_key: str, sub_key: str) -> str:
|
||||
# fmt: off
|
||||
MAPPING = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
|
||||
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13,
|
||||
]
|
||||
key = (img_key + sub_key).encode()
|
||||
return bytes([key[n] for n in MAPPING]).decode()
|
||||
|
||||
|
||||
def encode_value(value: str) -> str:
|
||||
chars = []
|
||||
|
||||
for c in value:
|
||||
if c in "!'()*":
|
||||
continue
|
||||
if (c.isascii() and c.isalnum()) or c in "-_.~":
|
||||
chars.append(c)
|
||||
else:
|
||||
for b in c.encode():
|
||||
chars.append(f"%{b:02X}")
|
||||
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def build_query(key: str, ts: int, params: List[Tuple[str, Any]]) -> str:
|
||||
params.append(("wts", str(ts)))
|
||||
params.sort(key=lambda p: p[0])
|
||||
|
||||
parts = []
|
||||
for name, value in params:
|
||||
parts.append(f"{name}={encode_value(str(value))}")
|
||||
query = "&".join(parts)
|
||||
|
||||
sign = hashlib.md5((query + key).encode()).hexdigest()
|
||||
query += f"&w_rid={sign}"
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def test_extract_key() -> None:
|
||||
url = "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png"
|
||||
key = extract_key(url)
|
||||
assert key == "7cd084941338484aae1ad9425b84077c"
|
||||
|
||||
|
||||
def test_make_key() -> None:
|
||||
img_key = "7cd084941338484aae1ad9425b84077c"
|
||||
sub_key = "4932caff0ff746eab6f01bf08b70ac45"
|
||||
expected = "ea1db124af3c7062474693fa704f4ff8"
|
||||
key = make_key(img_key, sub_key)
|
||||
assert key == expected
|
||||
|
||||
|
||||
def test_encode_value() -> None:
|
||||
expected = "-_-%20F%20%E5%93%94~"
|
||||
assert encode_value(")-_-( F**' 哔~!") == expected
|
||||
|
||||
|
||||
def test_build_query() -> None:
|
||||
img_key = "7cd084941338484aae1ad9425b84077c"
|
||||
sub_key = "4932caff0ff746eab6f01bf08b70ac45"
|
||||
|
||||
key = make_key(img_key, sub_key)
|
||||
ts = 1748867128
|
||||
params = [("foo", ")-_-( F**' 哔~!"), ("bar", 2333)]
|
||||
|
||||
expected = "bar=2333&foo=-_-%20F%20%E5%93%94~&wts=1748867128&w_rid=6ba96e28a3f09b40e704f1e4b4f8e3e3" # noqa
|
||||
assert build_query(key, ts, params) == expected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_extract_key()
|
||||
test_make_key()
|
||||
test_encode_value()
|
||||
test_build_query()
|
||||
@@ -131,9 +131,7 @@ class HeaderOptions(BaseModel):
|
||||
|
||||
class HeaderSettings(HeaderOptions):
|
||||
user_agent: str = (
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/89.0.4389.114 Safari/537.36'
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36' # noqa
|
||||
)
|
||||
cookie: str = ''
|
||||
|
||||
@@ -198,9 +196,7 @@ class RecorderSettings(RecorderOptions):
|
||||
fmp4_stream_timeout: int = 10
|
||||
read_timeout: int = 3
|
||||
disconnection_timeout: int = 600
|
||||
buffer_size: Annotated[
|
||||
int, Field(ge=4096, le=1024**2 * 512, multiple_of=2)
|
||||
] = 8192
|
||||
buffer_size: Annotated[int, Field(ge=4096, le=1024**2 * 512, multiple_of=2)] = 8192
|
||||
save_cover: bool = False
|
||||
cover_save_strategy: CoverSaveStrategy = CoverSaveStrategy.DEFAULT
|
||||
|
||||
@@ -315,7 +311,7 @@ class TaskOptions(BaseModel):
|
||||
|
||||
class TaskSettings(TaskOptions):
|
||||
# must use the real room id rather than the short room id!
|
||||
room_id: Annotated[int, Field(ge=1, lt=2**32)]
|
||||
room_id: Annotated[int, Field(ge=1, lt=2**100)]
|
||||
enable_monitor: bool = True
|
||||
enable_recorder: bool = True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user