Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047c098271 | ||
|
|
36f09499af | ||
|
|
62c1abfd24 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# 更新日志
|
||||
|
||||
## 1.2.3
|
||||
|
||||
- 修复直播间标题有特殊字符导致文件创建失败
|
||||
- 修复弹幕有特殊字符导致弹幕录制出错
|
||||
|
||||
## 1.2.2
|
||||
|
||||
- 避免 url 失效出现一直超时不能录制
|
||||
|
||||
## 1.2.1
|
||||
|
||||
- 兼容 flv 头不正确的直播流
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- 改进文件处理方式,文件录制完成后就进行处理。
|
||||
|
||||
@@ -146,8 +146,14 @@
|
||||
|
||||
[CHANGELOG](CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## 赞助 & 支持
|
||||
|
||||
如果觉得这个工具好用,对你有所帮助,可以投喂支持亿下哦~
|
||||
|
||||
投喂赞助 ☞ <https://afdian.net/@acgnhiki>
|
||||
|
||||
## Thanks
|
||||
|
||||
[](https://jb.gg/OpenSource)
|
||||
|
||||
48
setup.cfg
48
setup.cfg
@@ -36,41 +36,41 @@ include_package_data = True
|
||||
python_requires = >= 3.8
|
||||
install_requires =
|
||||
typing-extensions >= 3.10.0.0
|
||||
fastapi >= 0.65.2, < 0.66.0
|
||||
email_validator >=1.1.2, < 2.0.0
|
||||
typer >= 0.3.2, < 0.4.0
|
||||
aiohttp >= 3.6.2, < 3.7.0
|
||||
requests >= 2.24.0, < 2.25.0
|
||||
aiofiles >= 0.5.0, < 0.6.0
|
||||
tenacity >= 6.2.0, < 6.3.0
|
||||
colorama >= 0.4.3, < 0.5.0
|
||||
humanize >= 3.2.0, < 3.3.0
|
||||
tqdm >= 4.56.2, < 4.57.0
|
||||
attrs >= 21.2.0, < 21.3.0
|
||||
lxml >= 4.6.2, < 4.7.0
|
||||
fastapi >= 0.70.0, < 0.71.0
|
||||
email_validator >= 1.1.3, < 2.0.0
|
||||
typer >= 0.4.0, < 0.5.0
|
||||
aiohttp >= 3.8.1, < 4.0.0
|
||||
requests >= 2.24.0, < 3.0.0
|
||||
aiofiles >= 0.8.0, < 0.9.0
|
||||
tenacity >= 8.0.1, < 9.0.0
|
||||
colorama >= 0.4.4, < 0.5.0
|
||||
humanize >= 3.13.1, < 4.0.0
|
||||
tqdm >= 4.62.3, < 5.0.0
|
||||
attrs >= 21.2.0, < 22.0.0
|
||||
lxml >= 4.6.4, < 5.0.0
|
||||
toml >= 0.10.2, < 0.11.0
|
||||
psutil >= 5.8.0, < 5.9.0
|
||||
rx >= 3.1.1, < 3.2.0
|
||||
bitarray >= 2.2.5, < 2.3.0
|
||||
brotli >= 1.0.9, < 1.1.0
|
||||
uvicorn[standard] >=0.12.0, < 0.15.0
|
||||
psutil >= 5.8.0, < 6.0.0
|
||||
rx >= 3.2.0, < 4.0.0
|
||||
bitarray >= 2.2.5, < 3.0.0
|
||||
brotli >= 1.0.9, < 2.0.0
|
||||
uvicorn[standard] >= 0.15.0, < 0.16.0
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
flake8 >= 3.9.2, < 4.0.0
|
||||
flake8 >= 4.0.1, < 5.0.0
|
||||
mypy >= 0.910, < 1.000
|
||||
|
||||
setuptools >= 57.0.0, < 58.0.0
|
||||
setuptools >= 59.4.0, < 60.0.0
|
||||
wheel >= 0.37, < 0.38.0
|
||||
build >= 0.5.1, < 0.6.0
|
||||
build >= 0.7.0, < 0.8.0
|
||||
|
||||
# missing stub packages
|
||||
types-requests >= 2.25.1, < 2.26.0
|
||||
types-requests >= 2.26.1, < 3.0.0
|
||||
types-aiofiles >= 0.1.7, < 0.2.0
|
||||
types-toml >= 0.1.3, < 0.2.0
|
||||
types-setuptools >= 57.0.0, < 58.0.0
|
||||
types-toml >= 0.10.1, < 0.11.0
|
||||
types-setuptools >= 57.4.4, < 58.0.0
|
||||
|
||||
speedups = aiohttp[speedups] >= 3.6.2, < 3.7.0
|
||||
speedups = aiohttp[speedups] >= 3.8.1, < 4.0.0
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
__prog__ = 'blrec'
|
||||
__version__ = '1.2.0'
|
||||
__version__ = '1.2.3'
|
||||
__github__ = 'https://github.com/acgnhiki/blrec'
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any, Callable, Optional, Type, cast
|
||||
|
||||
from tenacity import wait_exponential, RetryCallState
|
||||
from tenacity import _utils
|
||||
from tenacity import compat as _compat
|
||||
|
||||
|
||||
class wait_exponential_for_same_exceptions(wait_exponential):
|
||||
@@ -25,7 +24,6 @@ class wait_exponential_for_same_exceptions(wait_exponential):
|
||||
self._prev_exc_ts: Optional[float] = None
|
||||
self._last_wait_time: float = 0
|
||||
|
||||
@_compat.wait_dunder_call_accept_old_params
|
||||
def __call__(self, retry_state: RetryCallState) -> float:
|
||||
if (
|
||||
retry_state.outcome is not None and
|
||||
|
||||
@@ -39,6 +39,7 @@ from ..bili.live import Live
|
||||
from ..bili.typing import QualityNumber
|
||||
from ..flv.stream_processor import StreamProcessor, BaseOutputFileManager
|
||||
from ..utils.mixins import AsyncCooperationMix, AsyncStoppableMixin
|
||||
from ..path import escape_path
|
||||
from ..flv.exceptions import FlvStreamCorruptedError
|
||||
from ..bili.exceptions import (
|
||||
LiveRoomHidden, LiveRoomLocked, LiveRoomEncrypted, NoStreamUrlAvailable
|
||||
@@ -317,6 +318,7 @@ class StreamRecorder(
|
||||
logger.warning(repr(e))
|
||||
except FlvStreamCorruptedError as e:
|
||||
logger.warning(repr(e))
|
||||
url = self._get_live_stream_url()
|
||||
|
||||
def _streaming(self, url: str) -> None:
|
||||
logger.debug('Getting the live stream...')
|
||||
@@ -500,10 +502,10 @@ class OutputFileManager(BaseOutputFileManager, AsyncCooperationMix):
|
||||
date_time = datetime.fromtimestamp(self._start_time)
|
||||
relpath = self.path_template.format(
|
||||
roomid=self._live.room_id,
|
||||
uname=self._live.user_info.name,
|
||||
title=self._live.room_info.title,
|
||||
area=self._live.room_info.area_name,
|
||||
parent_area=self._live.room_info.parent_area_name,
|
||||
uname=escape_path(self._live.user_info.name),
|
||||
title=escape_path(self._live.room_info.title),
|
||||
area=escape_path(self._live.room_info.area_name),
|
||||
parent_area=escape_path(self._live.room_info.parent_area_name),
|
||||
year=date_time.year,
|
||||
month=str(date_time.month).rjust(2, '0'),
|
||||
day=str(date_time.day).rjust(2, '0'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
import html
|
||||
import asyncio
|
||||
import logging
|
||||
import unicodedata
|
||||
from typing import AsyncIterator, Final, List, Any
|
||||
|
||||
from lxml import etree
|
||||
@@ -16,6 +18,9 @@ from .models import (
|
||||
__all__ = 'DanmakuReader', 'DanmakuWriter'
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DanmakuReader: # TODO rewrite
|
||||
def __init__(self, path: str) -> None:
|
||||
self._path = path
|
||||
@@ -146,7 +151,12 @@ class DanmakuWriter:
|
||||
'user': dm.uname,
|
||||
}
|
||||
elem = etree.Element('d', attrib=attrib)
|
||||
elem.text = dm.text
|
||||
try:
|
||||
elem.text = dm.text
|
||||
except ValueError:
|
||||
# ValueError: All strings must be XML compatible: Unicode or ASCII,
|
||||
# no NULL bytes or control characters
|
||||
elem.text = remove_control_characters(dm.text)
|
||||
return ' ' + etree.tostring(elem, encoding='utf8').decode() + '\n'
|
||||
|
||||
def _serialize_gift_send_record(self, record: GiftSendRecord) -> str:
|
||||
@@ -166,7 +176,10 @@ class DanmakuWriter:
|
||||
value_serializer=record_value_serializer,
|
||||
)
|
||||
elem = etree.Element('sc', attrib=attrib)
|
||||
elem.text = record.message
|
||||
try:
|
||||
elem.text = record.message
|
||||
except ValueError:
|
||||
elem.text = remove_control_characters(record.message)
|
||||
return ' ' + etree.tostring(elem, encoding='utf8').decode() + '\n'
|
||||
|
||||
|
||||
@@ -180,3 +193,7 @@ def record_value_serializer(
|
||||
if not isinstance(value, str):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
||||
def remove_control_characters(s: str) -> str:
|
||||
return ''.join(c for c in s if unicodedata.category(c) != 'Cc')
|
||||
|
||||
@@ -75,6 +75,8 @@ class DataAnalyser:
|
||||
self._keyframe_filepositions: List[int] = []
|
||||
self._resolution: Optional[Resolution] = None
|
||||
|
||||
self._has_audio = False
|
||||
self._has_video = False
|
||||
self._header_analysed = False
|
||||
self._audio_analysed = False
|
||||
self._video_analysed = False
|
||||
@@ -83,8 +85,6 @@ class DataAnalyser:
|
||||
assert not self._header_analysed
|
||||
self._header_analysed = True
|
||||
self._size_of_flv_header = header.size
|
||||
self._has_audio = header.has_audio()
|
||||
self._has_video = header.has_video()
|
||||
|
||||
def analyse_tag(self, tag: FlvTag) -> None:
|
||||
if is_audio_tag(tag):
|
||||
@@ -198,6 +198,7 @@ class DataAnalyser:
|
||||
|
||||
def _analyse_audio_tag(self, tag: AudioTag) -> None:
|
||||
if not self._audio_analysed:
|
||||
self._has_audio = True
|
||||
self._audio_analysed = True
|
||||
self._audio_codec_id = tag.sound_format.value
|
||||
self._audio_sample_rate = tag.sound_rate.value
|
||||
@@ -219,6 +220,7 @@ class DataAnalyser:
|
||||
pass
|
||||
|
||||
if not self._video_analysed:
|
||||
self._has_video = True
|
||||
self._video_analysed = True
|
||||
self._video_codec_id = tag.codec_id.value
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def non_negative_integer_validator(instance, attribute, value): # type: ignore
|
||||
raise ValueError(f"'{attribute}' has to be a non negative integer!")
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True, slots=True, frozen=True)
|
||||
@attr.s(auto_attribs=True, slots=True)
|
||||
class FlvHeader:
|
||||
signature: str
|
||||
version: int
|
||||
@@ -94,6 +94,18 @@ class FlvHeader:
|
||||
def has_audio(self) -> bool:
|
||||
return bool(self.type_flag & 0b0000_0100)
|
||||
|
||||
def set_video_flag(self, value: bool) -> None:
|
||||
if value:
|
||||
self.type_flag |= 0b0000_0001
|
||||
else:
|
||||
self.type_flag &= ~0b0000_0001
|
||||
|
||||
def set_audio_flag(self, value: bool) -> None:
|
||||
if value:
|
||||
self.type_flag |= 0b0000_0100
|
||||
else:
|
||||
self.type_flag &= ~0b0000_0100
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self.data_offset
|
||||
|
||||
@@ -208,7 +208,6 @@ class StreamProcessor:
|
||||
self._in_reader = FlvReaderWithTimestampFix(stream)
|
||||
flv_header = self._read_header()
|
||||
self._has_audio = flv_header.has_audio()
|
||||
assert flv_header.has_video(), 'no video in the stream!'
|
||||
|
||||
try:
|
||||
first_data_tag = self._read_first_data_tag()
|
||||
@@ -230,7 +229,7 @@ class StreamProcessor:
|
||||
self._new_file()
|
||||
|
||||
try:
|
||||
self._write_header(flv_header)
|
||||
self._write_header(self._ensure_header_correct(flv_header))
|
||||
self._transfer_meta_tags()
|
||||
self._transfer_first_data_tag(first_data_tag)
|
||||
except Exception:
|
||||
@@ -486,6 +485,15 @@ class StreamProcessor:
|
||||
self._size_updates.on_next(size)
|
||||
self._time_updates.on_next(tag.timestamp)
|
||||
|
||||
def _ensure_header_correct(self, header: FlvHeader) -> FlvHeader:
|
||||
header.set_video_flag(
|
||||
self._parameters_checker.last_video_header_tag is not None
|
||||
)
|
||||
header.set_audio_flag(
|
||||
self._parameters_checker.last_audio_header_tag is not None
|
||||
)
|
||||
return header
|
||||
|
||||
def _ensure_ts_correct(self, tag: FlvTag) -> None:
|
||||
if tag.timestamp + self._delta < 0:
|
||||
self._delta = -tag.timestamp
|
||||
|
||||
@@ -4,6 +4,7 @@ from .helpers import (
|
||||
danmaku_path,
|
||||
raw_danmaku_path,
|
||||
extra_metadata_path,
|
||||
escape_path,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,4 +14,5 @@ __all__ = (
|
||||
'danmaku_path',
|
||||
'raw_danmaku_path',
|
||||
'extra_metadata_path',
|
||||
'escape_path',
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import os
|
||||
from pathlib import PurePath
|
||||
|
||||
@@ -8,6 +9,7 @@ __all__ = (
|
||||
'danmaku_path',
|
||||
'raw_danmaku_path',
|
||||
'extra_metadata_path',
|
||||
'escape_path',
|
||||
)
|
||||
|
||||
|
||||
@@ -31,3 +33,7 @@ def raw_danmaku_path(video_path: str) -> str:
|
||||
|
||||
def extra_metadata_path(video_path: str) -> str:
|
||||
return video_path + '.meta.json'
|
||||
|
||||
|
||||
def escape_path(path: str) -> str:
|
||||
return re.sub(r'[\\/:*?"<>|]', '', path)
|
||||
|
||||
Reference in New Issue
Block a user