Compare commits

..

3 Commits

Author SHA1 Message Date
acgnhiki
047c098271 release: 1.2.3 2021-12-11 17:54:54 +08:00
acgnhiki
36f09499af release: v1.2.2 2021-12-04 14:24:15 +08:00
acgnhiki
62c1abfd24 release: v1.2.1 2021-11-20 10:38:49 +08:00
12 changed files with 104 additions and 38 deletions

View File

@@ -1,5 +1,18 @@
# 更新日志
## 1.2.3
- 修复直播间标题有特殊字符导致文件创建失败
- 修复弹幕有特殊字符导致弹幕录制出错
## 1.2.2
- 避免 url 失效出现一直超时不能录制
## 1.2.1
- 兼容 flv 头不正确的直播流
## 1.2.0
- 改进文件处理方式,文件录制完成后就进行处理。

View File

@@ -146,8 +146,14 @@
[CHANGELOG](CHANGELOG.md)
---
## 赞助 & 支持
如果觉得这个工具好用,对你有所帮助,可以投喂支持亿下哦~
投喂赞助 ☞ <https://afdian.net/@acgnhiki>
## Thanks
[![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://jb.gg/OpenSource)

View File

@@ -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

View File

@@ -1,4 +1,4 @@
__prog__ = 'blrec'
__version__ = '1.2.0'
__version__ = '1.2.3'
__github__ = 'https://github.com/acgnhiki/blrec'

View File

@@ -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

View File

@@ -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'),

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',
)

View File

@@ -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)