feat: enrich metadata

This commit is contained in:
acgnhik 2022-06-08 18:34:25 +08:00
parent 8b2444f6cc
commit 04aae19735
6 changed files with 99 additions and 30 deletions

View File

@ -1,5 +1,7 @@
import asyncio
import time
import json
import logging
import re
from typing import Dict, List, cast
@ -23,6 +25,7 @@ from .typing import ApiPlatform, QualityNumber, ResponseData, StreamCodec, Strea
__all__ = ('Live',)
logger = logging.getLogger(__name__)
_INFO_PATTERN = re.compile(
rb'<script>\s*window\.__NEPTUNE_IS_MY_WAIFU__\s*=\s*(\{.*?\})\s*</script>'
@ -163,6 +166,14 @@ class Live:
user_info_data = await self._appapi.get_user_info(uid)
return UserInfo.from_app_api_data(user_info_data)
async def get_timestamp(self) -> int:
try:
ts = await self.get_server_timestamp()
except Exception as e:
logger.warning(f'Failed to get timestamp from server: {repr(e)}')
ts = int(time.time())
return ts
async def get_server_timestamp(self) -> int:
# the timestamp on the server at the moment in seconds
return await self._webapi.get_timestamp()

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from collections import OrderedDict
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, Dict
from typing import TYPE_CHECKING, Any, Dict, Union
from .. import __github__, __prog__, __version__
from ..bili.helpers import get_quality_name
@ -24,9 +24,28 @@ class MetadataProvider:
return self._make_metadata()
def _make_metadata(self) -> Dict[str, Any]:
tz = timezone(timedelta(hours=8))
live_start_time = datetime.fromtimestamp(
self._live.room_info.live_start_time, timezone(timedelta(hours=8))
self._live.room_info.live_start_time, tz
)
if self._stream_recorder.stream_available_time is None:
stream_available_time: Union[datetime, str] = 'N/A'
else:
stream_available_time = datetime.fromtimestamp(
self._stream_recorder.stream_available_time, tz
)
if self._stream_recorder.hls_stream_available_time is None:
hls_stream_available_time: Union[datetime, str] = 'N/A'
else:
hls_stream_available_time = datetime.fromtimestamp(
self._stream_recorder.hls_stream_available_time, tz
)
if self._stream_recorder.record_start_time is None:
record_start_time: Union[datetime, str] = 'N/A'
else:
record_start_time = datetime.fromtimestamp(
self._stream_recorder.record_start_time, tz
)
assert self._stream_recorder.real_quality_number is not None
stream_quality = '{} ({}{})'.format(
@ -46,6 +65,9 @@ B站直播录像
分区{self._live.room_info.parent_area_name} - {self._live.room_info.area_name}
房间号{self._live.room_info.room_id}
开播时间{live_start_time}
开始推流时间: {stream_available_time}
HLS流可用时间: {hls_stream_available_time}
录像起始时间: {record_start_time}
流主机: {self._stream_recorder.stream_host}
流格式{self._stream_recorder.stream_format}
流画质{stream_quality}
@ -59,6 +81,9 @@ B站直播录像
'Area': self._live.room_info.area_name,
'ParentArea': self._live.room_info.parent_area_name,
'LiveStartTime': str(live_start_time),
'StreamAvailableTime': str(stream_available_time),
'HLSStreamAvailableTime': str(hls_stream_available_time),
'RecordStartTime': str(record_start_time),
'StreamHost': self._stream_recorder.stream_host,
'StreamFormat': self._stream_recorder.stream_format,
'StreamQuality': stream_quality,

View File

@ -1,14 +1,9 @@
import asyncio
import logging
import os
import re
import time
from datetime import datetime
from typing import Tuple
import aiohttp
from tenacity import retry, retry_if_exception_type, stop_after_delay, wait_exponential
from ..bili.live import Live
from ..path import escape_path
from ..utils.mixins import AsyncCooperationMixin
@ -26,28 +21,9 @@ class PathProvider(AsyncCooperationMixin):
self.path_template = path_template
def __call__(self) -> Tuple[str, int]:
timestamp = self._get_timestamp()
path = self._make_path(timestamp)
return path, timestamp
def _get_timestamp(self) -> int:
try:
return self._get_server_timestamp()
except Exception as e:
logger.warning(f'Failed to get server timestamp: {repr(e)}')
return self._get_local_timestamp()
def _get_local_timestamp(self) -> int:
return int(time.time())
@retry(
reraise=True,
retry=retry_if_exception_type((asyncio.TimeoutError, aiohttp.ClientError)),
wait=wait_exponential(multiplier=0.1, max=1),
stop=stop_after_delay(3),
)
def _get_server_timestamp(self) -> int:
return self._run_coroutine(self._live.get_server_timestamp())
ts = self._run_coroutine(self._live.get_timestamp())
path = self._make_path(ts)
return path, ts
def _make_path(self, timestamp: int) -> str:
date_time = datetime.fromtimestamp(timestamp)

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import time
import logging
from datetime import datetime
from typing import Iterator, Optional
@ -366,12 +367,14 @@ class Recorder(
async def on_live_ended(self, live: Live) -> None:
logger.info('The live has ended')
self._stream_available = False
self._stream_recorder.stream_available_time = None
await self._stop_recording()
self._print_waiting_message()
async def on_live_stream_available(self, live: Live) -> None:
logger.debug('The live stream becomes available')
self._stream_available = True
self._stream_recorder.stream_available_time = await live.get_timestamp()
await self._stream_recorder.start()
async def on_live_stream_reset(self, live: Live) -> None:

View File

@ -40,6 +40,7 @@ class StreamRecorder(
) -> None:
super().__init__()
self._live = live
self.stream_format = stream_format
self.fmp4_stream_timeout = fmp4_stream_timeout
@ -77,6 +78,26 @@ class StreamRecorder(
def stream_host(self) -> str:
return self._impl.stream_host
@property
def record_start_time(self) -> Optional[int]:
return self._impl.record_start_time
@property
def stream_available_time(self) -> Optional[int]:
return self._impl.stream_available_time
@stream_available_time.setter
def stream_available_time(self, ts: Optional[int]) -> None:
self._impl.stream_available_time = ts
@property
def hls_stream_available_time(self) -> Optional[int]:
return self._impl.hls_stream_available_time
@hls_stream_available_time.setter
def hls_stream_available_time(self, ts: Optional[int]) -> None:
self._impl.hls_stream_available_time = ts
@property
def dl_total(self) -> int:
return self._impl.dl_total
@ -206,11 +227,15 @@ class StreamRecorder(
return self._impl.stopped
async def _do_start(self) -> None:
self.hls_stream_available_time = None
stream_format = self.stream_format
if stream_format == 'fmp4':
logger.info('Waiting for the fmp4 stream becomes available...')
available = await self._wait_fmp4_stream()
if not available:
if available:
if self.stream_available_time is not None:
self.hls_stream_available_time = await self._live.get_timestamp()
else:
logger.warning(
'The specified stream format (fmp4) is not available '
f'in {self.fmp4_stream_timeout} seconcds, '
@ -265,6 +290,8 @@ class StreamRecorder(
return
self._impl.remove_listener(self)
stream_available_time = self._impl.stream_available_time
hls_stream_available_time = self._impl.hls_stream_available_time
self._impl = cls(
live=self._impl._live,
@ -279,5 +306,7 @@ class StreamRecorder(
)
self._impl.add_listener(self)
self._impl.stream_available_time = stream_available_time
self._impl.hls_stream_available_time = hls_stream_available_time
logger.debug(f'Changed stream recorder impl to {cls.__name__}')

View File

@ -110,6 +110,9 @@ class StreamRecorderImpl(
self._threads: List[Thread] = []
self._files: List[str] = []
self._stream_profile: StreamProfile = {}
self._record_start_time: Optional[int] = None
self._stream_available_time: Optional[int] = None
self._hls_stream_available_time: Optional[int] = None
def on_profile_updated(profile: StreamProfile) -> None:
self._stream_profile = profile
@ -119,6 +122,7 @@ class StreamRecorderImpl(
def on_file_opened(args: Tuple[str, int]) -> None:
logger.info(f"Video file created: '{args[0]}'")
self._files.append(args[0])
self._record_start_time = args[1]
self._emit_event('video_file_created', *args)
def on_file_closed(path: str) -> None:
@ -136,6 +140,26 @@ class StreamRecorderImpl(
def stream_host(self) -> str:
return self._stream_url_resolver.stream_host
@property
def record_start_time(self) -> Optional[int]:
return self._record_start_time
@property
def stream_available_time(self) -> Optional[int]:
return self._stream_available_time
@stream_available_time.setter
def stream_available_time(self, ts: Optional[int]) -> None:
self._stream_available_time = ts
@property
def hls_stream_available_time(self) -> Optional[int]:
return self._hls_stream_available_time
@hls_stream_available_time.setter
def hls_stream_available_time(self, ts: Optional[int]) -> None:
self._hls_stream_available_time = ts
@property
def dl_total(self) -> int:
return self._dl_statistics.count
@ -266,6 +290,7 @@ class StreamRecorderImpl(
def _reset(self) -> None:
self._files.clear()
self._stream_profile = {}
self._record_start_time = None
async def _do_start(self) -> None:
logger.debug('Starting stream recorder...')