feat: add CoverImageDownloadedEvent and PostprocessingCompletedEvent

This commit is contained in:
acgnhik 2023-03-30 10:59:06 +08:00
parent edb292e8ed
commit 1ac194ef4d
19 changed files with 185 additions and 60 deletions

View File

@ -1,4 +1,3 @@
import asyncio
import logging
from enum import Enum
from threading import Lock
@ -9,7 +8,8 @@ import aiohttp
from tenacity import retry, stop_after_attempt, wait_fixed
from blrec.bili.live import Live
from blrec.exception import exception_callback
from blrec.event.event_emitter import EventEmitter, EventListener
from blrec.exception import submit_exception
from blrec.logging.room_id import aio_task_with_room_id
from blrec.path import cover_path
from blrec.utils.hash import sha1sum
@ -17,12 +17,17 @@ from blrec.utils.mixins import SwitchableMixin
from .stream_recorder import StreamRecorder, StreamRecorderEventListener
__all__ = ('CoverDownloader',)
__all__ = 'CoverDownloader', 'CoverDownloaderEventListener'
logger = logging.getLogger(__name__)
class CoverDownloaderEventListener(EventListener):
async def on_cover_image_downloaded(self, path: str) -> None:
...
class CoverSaveStrategy(Enum):
DEFAULT = 'default'
DEDUP = 'dedup'
@ -35,7 +40,11 @@ class CoverSaveStrategy(Enum):
return str(self)
class CoverDownloader(StreamRecorderEventListener, SwitchableMixin):
class CoverDownloader(
EventEmitter[CoverDownloaderEventListener],
StreamRecorderEventListener,
SwitchableMixin,
):
def __init__(
self,
live: Live,
@ -65,8 +74,7 @@ class CoverDownloader(StreamRecorderEventListener, SwitchableMixin):
with self._lock:
if not self.save_cover:
return
task = asyncio.create_task(self._save_cover(video_path))
task.add_done_callback(exception_callback)
await self._save_cover(video_path)
@aio_task_with_room_id
async def _save_cover(self, video_path: str) -> None:
@ -85,8 +93,10 @@ class CoverDownloader(StreamRecorderEventListener, SwitchableMixin):
self._sha1_set.add(sha1)
except Exception as e:
logger.error(f'Failed to save cover image: {repr(e)}')
submit_exception(e)
else:
logger.info(f'Saved cover image: {path}')
await self._emit('cover_image_downloaded', path)
@retry(reraise=True, wait=wait_fixed(1), stop=stop_after_attempt(3))
async def _fetch_cover(self, url: str) -> bytes:

View File

@ -17,7 +17,11 @@ from blrec.flv.operators import MetaData, StreamProfile
from blrec.setting.typing import RecordingMode
from blrec.utils.mixins import AsyncStoppableMixin
from .cover_downloader import CoverDownloader, CoverSaveStrategy
from .cover_downloader import (
CoverDownloader,
CoverDownloaderEventListener,
CoverSaveStrategy,
)
from .danmaku_dumper import DanmakuDumper, DanmakuDumperEventListener
from .danmaku_receiver import DanmakuReceiver
from .raw_danmaku_dumper import RawDanmakuDumper, RawDanmakuDumperEventListener
@ -60,6 +64,9 @@ class RecorderEventListener(EventListener):
) -> None:
...
async def on_cover_image_downloaded(self, recorder: Recorder, path: str) -> None:
...
class Recorder(
EventEmitter[RecorderEventListener],
@ -67,6 +74,7 @@ class Recorder(
AsyncStoppableMixin,
DanmakuDumperEventListener,
RawDanmakuDumperEventListener,
CoverDownloaderEventListener,
StreamRecorderEventListener,
):
def __init__(
@ -415,6 +423,9 @@ class Recorder(
async def on_raw_danmaku_file_completed(self, path: str) -> None:
await self._emit('raw_danmaku_file_completed', self, path)
async def on_cover_image_downloaded(self, path: str) -> None:
await self._emit('cover_image_downloaded', self, path)
async def on_stream_recording_completed(self) -> None:
logger.debug('Stream recording completed')
await self._stop_recording()
@ -423,7 +434,7 @@ class Recorder(
self._live_monitor.add_listener(self)
self._danmaku_dumper.add_listener(self)
self._raw_danmaku_dumper.add_listener(self)
self._stream_recorder.add_listener(self)
self._cover_downloader.add_listener(self)
logger.debug('Started recorder')
self._print_live_info()
@ -438,7 +449,7 @@ class Recorder(
self._live_monitor.remove_listener(self)
self._danmaku_dumper.remove_listener(self)
self._raw_danmaku_dumper.remove_listener(self)
self._stream_recorder.remove_listener(self)
self._cover_downloader.remove_listener(self)
logger.debug('Stopped recorder')
async def _start_recording(self) -> None:
@ -452,6 +463,7 @@ class Recorder(
self._danmaku_dumper.enable()
self._danmaku_receiver.start()
self._cover_downloader.enable()
self._stream_recorder.add_listener(self)
await self._prepare()
if self._stream_available:
@ -472,6 +484,7 @@ class Recorder(
self._danmaku_dumper.disable()
self._danmaku_receiver.stop()
self._cover_downloader.disable()
self._stream_recorder.remove_listener(self)
if self._stopped:
logger.info('Recording Cancelled')

View File

@ -10,6 +10,6 @@
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="runtime.cc23f2772d9f3147.js" type="module"></script><script src="polyfills.4e5433063877ea34.js" type="module"></script><script src="main.f21b7d831ad9cafb.js" type="module"></script>
<script src="runtime.ede29de4183c4835.js" type="module"></script><script src="polyfills.4e5433063877ea34.js" type="module"></script><script src="main.f21b7d831ad9cafb.js" type="module"></script>
</body></html>

View File

@ -1,6 +1,6 @@
{
"configVersion": 1,
"timestamp": 1679555539897,
"timestamp": 1679555877254,
"index": "/index.html",
"assetGroups": [
{
@ -15,14 +15,14 @@
"/287.bce56b4b2bd030eb.js",
"/386.2404f3bc252e1df3.js",
"/503.6553f508f4a9247d.js",
"/548.b10ecff8d5cc6ecb.js",
"/548.fd78f28272b50729.js",
"/688.7032fddba7983cf6.js",
"/common.1fc175bce139f4df.js",
"/index.html",
"/main.f21b7d831ad9cafb.js",
"/manifest.webmanifest",
"/polyfills.4e5433063877ea34.js",
"/runtime.cc23f2772d9f3147.js",
"/runtime.ede29de4183c4835.js",
"/styles.ae81e04dfa5b2860.css"
],
"patterns": []
@ -1638,7 +1638,7 @@
"/287.bce56b4b2bd030eb.js": "094898df47377213f62f6f207fa65111631fb85f",
"/386.2404f3bc252e1df3.js": "f937945645579b9651be2666f70cec2c5de4e367",
"/503.6553f508f4a9247d.js": "0878ea0e91bfd5458dd55875561e91060ecb0837",
"/548.b10ecff8d5cc6ecb.js": "cf3c34c2ab63adab39f7cffe571eb9559c7d6843",
"/548.fd78f28272b50729.js": "8d78077fa8ecc33314065e4e88aae6398b04af61",
"/688.7032fddba7983cf6.js": "eae55044529782a51b7e534365255bbfa5522b05",
"/assets/animal/panda.js": "fec2868bb3053dd2da45f96bbcb86d5116ed72b1",
"/assets/animal/panda.svg": "bebd302cdc601e0ead3a6d2710acf8753f3d83b1",
@ -3234,11 +3234,11 @@
"/assets/twotone/warning.js": "fb2d7ea232f3a99bf8f080dbc94c65699232ac01",
"/assets/twotone/warning.svg": "8c7a2d3e765a2e7dd58ac674870c6655cecb0068",
"/common.1fc175bce139f4df.js": "af1775164711ec49e5c3a91ee45bd77509c17c54",
"/index.html": "093a74f9ff6a876df2a000be31c839c17cd7bbfc",
"/index.html": "7ec3f85e632f8c350df664fe8b676d7d8145ce7a",
"/main.f21b7d831ad9cafb.js": "fc51efa446c2ac21ee17e165217dd3faeacc5290",
"/manifest.webmanifest": "62c1cb8c5ad2af551a956b97013ab55ce77dd586",
"/polyfills.4e5433063877ea34.js": "68159ab99e0608976404a17132f60b5ceb6f12d2",
"/runtime.cc23f2772d9f3147.js": "0b62308c4feae2804da66bbf190bc1eef7e6b512",
"/runtime.ede29de4183c4835.js": "67c63422212cd6ccb620031b831eacb361b0ee9f",
"/styles.ae81e04dfa5b2860.css": "5933b4f1c4d8fcc1891b68940ee78af4091472b7"
},
"navigationUrls": [

View File

@ -1 +0,0 @@
(()=>{"use strict";var e,v={},m={};function r(e){var n=m[e];if(void 0!==n)return n.exports;var t=m[e]={exports:{}};return v[e](t,t.exports,r),t.exports}r.m=v,e=[],r.O=(n,t,i,o)=>{if(!t){var a=1/0;for(f=0;f<e.length;f++){for(var[t,i,o]=e[f],c=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(p=>r.O[p](t[d]))?t.splice(d--,1):(c=!1,o<a&&(a=o));if(c){e.splice(f--,1);var l=i();void 0!==l&&(n=l)}}return n}o=o||0;for(var f=e.length;f>0&&e[f-1][2]>o;f--)e[f]=e[f-1];e[f]=[t,i,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>(592===e?"common":e)+"."+{103:"4a2aea63cc3bf42b",287:"bce56b4b2bd030eb",386:"2404f3bc252e1df3",503:"6553f508f4a9247d",548:"b10ecff8d5cc6ecb",592:"1fc175bce139f4df",688:"7032fddba7983cf6"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="blrec:";r.l=(t,i,o,f)=>{if(e[t])e[t].push(i);else{var a,c;if(void 0!==o)for(var d=document.getElementsByTagName("script"),l=0;l<d.length;l++){var u=d[l];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==n+o){a=u;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+o),a.src=r.tu(t)),e[t]=[i];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:n=>n},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(i,o)=>{var f=r.o(e,i)?e[i]:void 0;if(0!==f)if(f)o.push(f[2]);else if(666!=i){var a=new Promise((u,s)=>f=e[i]=[u,s]);o.push(f[2]=a);var c=r.p+r.u(i),d=new Error;r.l(c,u=>{if(r.o(e,i)&&(0!==(f=e[i])&&(e[i]=void 0),f)){var s=u&&("load"===u.type?"missing":u.type),b=u&&u.target&&u.target.src;d.message="Loading chunk "+i+" failed.\n("+s+": "+b+")",d.name="ChunkLoadError",d.type=s,d.request=b,f[1](d)}},"chunk-"+i,i)}else e[i]=0},r.O.j=i=>0===e[i];var n=(i,o)=>{var d,l,[f,a,c]=o,u=0;if(f.some(b=>0!==e[b])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(c)var s=c(r)}for(i&&i(o);u<f.length;u++)r.o(e,l=f[u])&&e[l]&&e[l][0](),e[l]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();

View File

@ -0,0 +1 @@
(()=>{"use strict";var e,v={},m={};function r(e){var n=m[e];if(void 0!==n)return n.exports;var t=m[e]={exports:{}};return v[e](t,t.exports,r),t.exports}r.m=v,e=[],r.O=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],c=!0,u=0;u<t.length;u++)(!1&o||a>=o)&&Object.keys(r.O).every(p=>r.O[p](t[u]))?t.splice(u--,1):(c=!1,o<a&&(a=o));if(c){e.splice(i--,1);var l=f();void 0!==l&&(n=l)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>(592===e?"common":e)+"."+{103:"4a2aea63cc3bf42b",287:"bce56b4b2bd030eb",386:"2404f3bc252e1df3",503:"6553f508f4a9247d",548:"fd78f28272b50729",592:"1fc175bce139f4df",688:"7032fddba7983cf6"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="blrec:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,c;if(void 0!==o)for(var u=document.getElementsByTagName("script"),l=0;l<u.length;l++){var d=u[l];if(d.getAttribute("src")==t||d.getAttribute("data-webpack")==n+o){a=d;break}}a||(c=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+o),a.src=r.tu(t)),e[t]=[f];var s=(g,p)=>{a.onerror=a.onload=null,clearTimeout(b);var _=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),_&&_.forEach(h=>h(p)),g)return g(p)},b=setTimeout(s.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=s.bind(null,a.onerror),a.onload=s.bind(null,a.onload),c&&document.head.appendChild(a)}}})(),r.r=e=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:n=>n},typeof trustedTypes<"u"&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((d,s)=>i=e[f]=[d,s]);o.push(i[2]=a);var c=r.p+r.u(f),u=new Error;r.l(c,d=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var s=d&&("load"===d.type?"missing":d.type),b=d&&d.target&&d.target.src;u.message="Loading chunk "+f+" failed.\n("+s+": "+b+")",u.name="ChunkLoadError",u.type=s,u.request=b,i[1](u)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var u,l,[i,a,c]=o,d=0;if(i.some(b=>0!==e[b])){for(u in a)r.o(a,u)&&(r.m[u]=a[u]);if(c)var s=c(r)}for(f&&f(o);d<i.length;d++)r.o(e,l=i[d])&&e[l]&&e[l][0](),e[l]=0;return r.O(s)},t=self.webpackChunkblrec=self.webpackChunkblrec||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();

View File

@ -26,9 +26,13 @@ from .models import (
RawDanmakuFileCreatedEvent,
RawDanmakuFileCreatedEventData,
RawDanmakuFileCompletedEvent,
CoverImageDownloadedEvent,
CoverImageDownloadedEventData,
RawDanmakuFileCompletedEventData,
VideoPostprocessingCompletedEvent,
VideoPostprocessingCompletedEventData,
PostprocessingCompletedEvent,
PostprocessingCompletedEventData,
SpaceNoEnoughEvent,
SpaceNoEnoughEventData,
Error,
@ -67,8 +71,12 @@ __all__ = (
'RawDanmakuFileCreatedEventData',
'RawDanmakuFileCompletedEvent',
'RawDanmakuFileCompletedEventData',
'CoverImageDownloadedEvent',
'CoverImageDownloadedEventData',
'VideoPostprocessingCompletedEvent',
'VideoPostprocessingCompletedEventData',
'PostprocessingCompletedEvent',
'PostprocessingCompletedEventData',
'SpaceNoEnoughEvent',
'SpaceNoEnoughEventData',
'Error',

View File

@ -1,5 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import List, TYPE_CHECKING
from .event_center import EventCenter
from .models import (
@ -27,8 +27,12 @@ from .models import (
RawDanmakuFileCreatedEventData,
RawDanmakuFileCompletedEvent,
RawDanmakuFileCompletedEventData,
CoverImageDownloadedEvent,
CoverImageDownloadedEventData,
VideoPostprocessingCompletedEvent,
VideoPostprocessingCompletedEventData,
PostprocessingCompletedEvent,
PostprocessingCompletedEventData,
SpaceNoEnoughEvent,
SpaceNoEnoughEventData,
)
@ -38,6 +42,7 @@ from ..bili.live_monitor import LiveEventListener
from ..core.recorder import RecorderEventListener
from ..disk_space import SpaceEventListener
from ..postprocess import PostprocessorEventListener
if TYPE_CHECKING:
from ..bili.live_monitor import LiveMonitor
from ..core.recorder import Recorder
@ -45,11 +50,7 @@ if TYPE_CHECKING:
from ..postprocess import Postprocessor
__all__ = (
'LiveEventSubmitter',
'SpaceEventSubmitter',
'PostprocessorEventSubmitter',
)
__all__ = ('LiveEventSubmitter', 'SpaceEventSubmitter', 'PostprocessorEventSubmitter')
event_center = EventCenter.get_instance()
@ -89,33 +90,23 @@ class RecorderEventSubmitter(RecorderEventListener):
data = RecordingCancelledEventData(recorder.live.room_info)
event_center.submit(RecordingCancelledEvent.from_data(data))
async def on_video_file_created(
self, recorder: Recorder, path: str
) -> None:
async def on_video_file_created(self, recorder: Recorder, path: str) -> None:
data = VideoFileCreatedEventData(recorder.live.room_id, path)
event_center.submit(VideoFileCreatedEvent.from_data(data))
async def on_video_file_completed(
self, recorder: Recorder, path: str
) -> None:
async def on_video_file_completed(self, recorder: Recorder, path: str) -> None:
data = VideoFileCompletedEventData(recorder.live.room_id, path)
event_center.submit(VideoFileCompletedEvent.from_data(data))
async def on_danmaku_file_created(
self, recorder: Recorder, path: str
) -> None:
async def on_danmaku_file_created(self, recorder: Recorder, path: str) -> None:
data = DanmakuFileCreatedEventData(recorder.live.room_id, path)
event_center.submit(DanmakuFileCreatedEvent.from_data(data))
async def on_danmaku_file_completed(
self, recorder: Recorder, path: str
) -> None:
async def on_danmaku_file_completed(self, recorder: Recorder, path: str) -> None:
data = DanmakuFileCompletedEventData(recorder.live.room_id, path)
event_center.submit(DanmakuFileCompletedEvent.from_data(data))
async def on_raw_danmaku_file_created(
self, recorder: Recorder, path: str
) -> None:
async def on_raw_danmaku_file_created(self, recorder: Recorder, path: str) -> None:
data = RawDanmakuFileCreatedEventData(recorder.live.room_id, path)
event_center.submit(RawDanmakuFileCreatedEvent.from_data(data))
@ -125,6 +116,10 @@ class RecorderEventSubmitter(RecorderEventListener):
data = RawDanmakuFileCompletedEventData(recorder.live.room_id, path)
event_center.submit(RawDanmakuFileCompletedEvent.from_data(data))
async def on_cover_image_downloaded(self, recorder: Recorder, path: str) -> None:
data = CoverImageDownloadedEventData(recorder.live.room_id, path)
event_center.submit(CoverImageDownloadedEvent.from_data(data))
class PostprocessorEventSubmitter(PostprocessorEventListener):
def __init__(self, postprocessor: Postprocessor) -> None:
@ -139,6 +134,14 @@ class PostprocessorEventSubmitter(PostprocessorEventListener):
)
event_center.submit(VideoPostprocessingCompletedEvent.from_data(data))
async def on_postprocessing_completed(
self, postprocessor: Postprocessor, files: List[str]
) -> None:
data = PostprocessingCompletedEventData(
postprocessor.recorder.live.room_id, files
)
event_center.submit(PostprocessingCompletedEvent.from_data(data))
class SpaceEventSubmitter(SpaceEventListener):
def __init__(self, space_monitor: SpaceMonitor) -> None:

View File

@ -1,16 +1,17 @@
from __future__ import annotations
from abc import ABC
import uuid
from abc import ABC
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Generic, TYPE_CHECKING, Type, TypeVar
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Type, TypeVar
import attr
if TYPE_CHECKING:
from ..bili.models import UserInfo, RoomInfo
from ..disk_space.space_monitor import DiskUsage
from ..exception import format_exception
from ..exception import format_exception
_D = TypeVar('_D', bound='BaseEventData')
_E = TypeVar('_E')
@ -32,9 +33,7 @@ class BaseEvent(Generic[_D]):
@classmethod
def from_data(cls: Type[_E], data: _D) -> _E:
return cls( # type: ignore
id=uuid.uuid1(),
date=datetime.now(timezone(timedelta(hours=8))),
data=data,
id=uuid.uuid1(), date=datetime.now(timezone(timedelta(hours=8))), data=data
)
def asdict(self) -> Dict[str, Any]:
@ -181,13 +180,23 @@ class RawDanmakuFileCompletedEventData(BaseEventData):
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)
class RawDanmakuFileCompletedEvent(
BaseEvent[RawDanmakuFileCompletedEventData]
):
class RawDanmakuFileCompletedEvent(BaseEvent[RawDanmakuFileCompletedEventData]):
type: str = 'RawDanmakuFileCompletedEvent'
data: RawDanmakuFileCompletedEventData
@attr.s(auto_attribs=True, slots=True, frozen=True)
class CoverImageDownloadedEventData(BaseEventData):
room_id: int
path: str
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)
class CoverImageDownloadedEvent(BaseEvent[CoverImageDownloadedEventData]):
type: str = 'CoverImageDownloadedEvent'
data: CoverImageDownloadedEventData
@attr.s(auto_attribs=True, slots=True, frozen=True)
class VideoPostprocessingCompletedEventData(BaseEventData):
room_id: int
@ -202,6 +211,18 @@ class VideoPostprocessingCompletedEvent(
data: VideoPostprocessingCompletedEventData
@attr.s(auto_attribs=True, slots=True, frozen=True)
class PostprocessingCompletedEventData(BaseEventData):
room_id: int
files: List[str]
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)
class PostprocessingCompletedEvent(BaseEvent[PostprocessingCompletedEventData]):
type: str = 'PostprocessingCompletedEvent'
data: PostprocessingCompletedEventData
@attr.s(auto_attribs=True, slots=True, frozen=True)
class SpaceNoEnoughEventData(BaseEventData):
path: str
@ -222,9 +243,7 @@ class ErrorData(BaseEventData):
@classmethod
def from_exc(cls, exc: BaseException) -> ErrorData:
return cls(
name=type(exc).__name__, detail=format_exception(exc),
)
return cls(name=type(exc).__name__, detail=format_exception(exc))
@attr.s(auto_attribs=True, slots=True, frozen=True, kw_only=True)

View File

@ -26,8 +26,12 @@ from .models import (
RawDanmakuFileCreatedEventData,
RawDanmakuFileCompletedEvent,
RawDanmakuFileCompletedEventData,
CoverImageDownloadedEvent,
CoverImageDownloadedEventData,
VideoPostprocessingCompletedEvent,
VideoPostprocessingCompletedEventData,
PostprocessingCompletedEvent,
PostprocessingCompletedEventData,
SpaceNoEnoughEvent,
SpaceNoEnoughEventData,
Error,
@ -48,7 +52,9 @@ Event = Union[
DanmakuFileCompletedEvent,
RawDanmakuFileCreatedEvent,
RawDanmakuFileCompletedEvent,
CoverImageDownloadedEvent,
VideoPostprocessingCompletedEvent,
PostprocessingCompletedEvent,
SpaceNoEnoughEvent,
Error,
]
@ -66,7 +72,9 @@ EventData = Union[
DanmakuFileCompletedEventData,
RawDanmakuFileCreatedEventData,
RawDanmakuFileCompletedEventData,
CoverImageDownloadedEventData,
VideoPostprocessingCompletedEventData,
PostprocessingCompletedEventData,
SpaceNoEnoughEventData,
ErrorData,
]

View File

@ -44,18 +44,22 @@ async def discard_dir(path: str, log_level: Literal['INFO', 'DEBUG'] = 'INFO') -
logger.log(logging.getLevelName(log_level), f'Deleted {path!r}')
async def copy_files_related(video_path: str) -> None:
loop = asyncio.get_running_loop()
dirname = os.path.dirname(video_path)
for src_path in [
def files_related(video_path: str) -> Iterable[str]:
for path in [
danmaku_path(video_path),
raw_danmaku_path(video_path),
cover_path(video_path, ext='jpg'),
cover_path(video_path, ext='png'),
]:
if not os.path.isfile(src_path):
continue
if os.path.isfile(path):
yield path
async def copy_files_related(video_path: str) -> None:
loop = asyncio.get_running_loop()
dirname = os.path.dirname(video_path)
for src_path in files_related(video_path):
root, ext = os.path.splitext(src_path)
dst_path = PurePath(dirname).with_suffix(ext)
try:

View File

@ -20,7 +20,13 @@ from ..logging.room_id import aio_task_with_room_id
from ..path import danmaku_path, extra_metadata_path, record_metadata_path
from ..utils.mixins import AsyncCooperationMixin, AsyncStoppableMixin, SupportDebugMixin
from .ffmpeg_metadata import make_metadata_file
from .helpers import copy_files_related, discard_dir, discard_file, get_extra_metadata
from .helpers import (
copy_files_related,
discard_dir,
discard_file,
files_related,
get_extra_metadata,
)
from .models import DeleteStrategy, PostprocessorStatus
from .remux import RemuxingProgress, RemuxingResult, remux_video
from .typing import Progress
@ -44,6 +50,11 @@ class PostprocessorEventListener(EventListener):
) -> None:
...
async def on_postprocessing_completed(
self, postprocessor: Postprocessor, files: List[str]
) -> None:
...
class Postprocessor(
EventEmitter[PostprocessorEventListener],
@ -195,6 +206,9 @@ class Postprocessor(
await self._emit(
'video_postprocessing_completed', self, result_path
)
files = [result_path, *files_related(result_path)]
await self._emit('postprocessing_completed', self, files)
except Exception as exc:
submit_exception(exc)
finally:

View File

@ -627,7 +627,9 @@ class WebHookEventSettings(BaseModel):
danmaku_file_completed: bool = True
raw_danmaku_file_created: bool = True
raw_danmaku_file_completed: bool = True
cover_image_downloaded: bool = True
video_postprocessing_completed: bool = True
postprocessing_completed: bool = True
space_no_enough: bool = True
error_occurred: bool = True

View File

@ -18,8 +18,10 @@ from ..event import (
DanmakuFileCompletedEvent,
RawDanmakuFileCreatedEvent,
RawDanmakuFileCompletedEvent,
CoverImageDownloadedEvent,
SpaceNoEnoughEvent,
VideoPostprocessingCompletedEvent,
PostprocessingCompletedEvent,
)
from ..event.typing import Event
@ -64,8 +66,12 @@ class WebHook:
types.add(RawDanmakuFileCreatedEvent)
if settings.raw_danmaku_file_completed:
types.add(RawDanmakuFileCompletedEvent)
if settings.cover_image_downloaded:
types.add(CoverImageDownloadedEvent)
if settings.video_postprocessing_completed:
types.add(VideoPostprocessingCompletedEvent)
if settings.postprocessing_completed:
types.add(PostprocessingCompletedEvent)
if settings.space_no_enough:
types.add(SpaceNoEnoughEvent)

View File

@ -13,8 +13,10 @@ export type Event =
| DanmakuFileCompletedEvent
| RawDanmakuFileCreatedEvent
| RawDanmakuFileCompletedEvent
| CoverImageDownloadedEvent
| SpaceNoEnoughEvent
| VideoPostprocessingCompletedEvent;
| VideoPostprocessingCompletedEvent
| PostprocessingCompletedEvent;
export interface LiveBeganEvent {
readonly type: 'LiveBeganEvent';
@ -100,6 +102,14 @@ export interface RawDanmakuFileCompletedEvent {
};
}
export interface CoverImageDownloadedEvent {
readonly type: 'CoverImageDownloadedEvent';
readonly data: {
room_id: number;
path: string;
};
}
export interface VideoPostprocessingCompletedEvent {
readonly type: 'VideoPostprocessingCompletedEvent';
readonly data: {
@ -108,6 +118,14 @@ export interface VideoPostprocessingCompletedEvent {
};
}
export interface PostprocessingCompletedEvent {
readonly type: 'PostprocessingCompletedEvent';
readonly data: {
room_id: number;
files: string[];
};
}
export interface SpaceNoEnoughEvent {
readonly type: 'SpaceNoEnoughEvent';
readonly data: {

View File

@ -377,7 +377,9 @@ export interface WebhookEventSettings {
danmakuFileCompleted: boolean;
rawDanmakuFileCreated: boolean;
rawDanmakuFileCompleted: boolean;
coverImageDownloaded: boolean;
videoPostprocessingCompleted: boolean;
postprocessingCompleted: boolean;
spaceNoEnough: boolean;
errorOccurred: boolean;
}

View File

@ -123,6 +123,13 @@
>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item">
<nz-form-control class="setting-control checkbox">
<label nz-checkbox formControlName="coverImageDownloaded"
>直播封面下载完成</label
>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item">
<nz-form-control class="setting-control checkbox">
<label nz-checkbox formControlName="videoPostprocessingCompleted"
@ -130,6 +137,13 @@
>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item">
<nz-form-control class="setting-control checkbox">
<label nz-checkbox formControlName="postprocessingCompleted"
>文件后处理完成</label
>
</nz-form-control>
</nz-form-item>
<nz-form-item class="setting-item">
<nz-form-control class="setting-control checkbox">
<label nz-checkbox formControlName="spaceNoEnough"

View File

@ -30,7 +30,9 @@ const DEFAULT_SETTINGS = {
danmakuFileCompleted: true,
rawDanmakuFileCreated: true,
rawDanmakuFileCompleted: true,
coverImageDownloaded: true,
videoPostprocessingCompleted: true,
postprocessingCompleted: true,
spaceNoEnough: true,
errorOccurred: true,
} as const;
@ -75,7 +77,9 @@ export class WebhookEditDialogComponent implements OnChanges {
danmakuFileCompleted: [''],
rawDanmakuFileCreated: [''],
rawDanmakuFileCompleted: [''],
coverImageDownloaded: [''],
videoPostprocessingCompleted: [''],
postprocessingCompleted: [''],
spaceNoEnough: [''],
errorOccurred: [''],
});