mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
13 KiB
Rust
342 lines
13 KiB
Rust
use futures::StreamExt;
|
|
|
|
use super::{ClientApiImpl, RoomActor};
|
|
use crate::impls::ApiError;
|
|
|
|
pub type BilibiliLiveDanmakuStream = std::pin::Pin<
|
|
Box<
|
|
dyn futures::Stream<
|
|
Item = Result<
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEvent,
|
|
ApiError,
|
|
>,
|
|
> + Send
|
|
+ 'static,
|
|
>,
|
|
>;
|
|
|
|
fn live_danmaku_event_to_proto(
|
|
event: synctv_core::provider::BilibiliLiveDanmakuEvent,
|
|
) -> synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEvent {
|
|
let r#type = match event.kind {
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::Unspecified => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::Unspecified
|
|
}
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::Chat => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::Chat
|
|
}
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::UserEnter => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::UserEnter
|
|
}
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::Gift => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::Gift
|
|
}
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::Heartbeat => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::Heartbeat
|
|
}
|
|
synctv_core::provider::BilibiliLiveDanmakuEventKind::Unknown => {
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEventType::Unknown
|
|
}
|
|
};
|
|
synctv_proto::playback_provider::bilibili::BilibiliLiveDanmakuEvent {
|
|
format: event.format,
|
|
event_type: event.event_type,
|
|
user: event.user,
|
|
message: event.message,
|
|
timestamp: event.timestamp,
|
|
gift_name: event.gift_name,
|
|
gift_count: event.gift_count,
|
|
online_count: event.online_count,
|
|
r#type: r#type as i32,
|
|
}
|
|
}
|
|
|
|
fn guest_is_current_bilibili_live_media(
|
|
state: &synctv_core::models::RoomPlaybackState,
|
|
media_id: &synctv_core::models::MediaId,
|
|
) -> bool {
|
|
state.playing_media_id.as_ref() == Some(media_id)
|
|
}
|
|
|
|
fn guest_is_current_bilibili_dynamic_live(
|
|
state: &synctv_core::models::RoomPlaybackState,
|
|
playlist_id: &synctv_core::models::PlaylistId,
|
|
target: &synctv_core::models::ProviderTarget,
|
|
) -> bool {
|
|
state.playing_playlist_id.as_ref() == Some(playlist_id) && state.target.as_ref() == Some(target)
|
|
}
|
|
|
|
fn require_current_bilibili_live_danmaku_target(target_is_current: bool) -> Result<(), ApiError> {
|
|
if target_is_current {
|
|
Ok(())
|
|
} else {
|
|
Err(ApiError::Authorization(
|
|
"Bilibili live danmaku is available only for the current playback".to_string(),
|
|
))
|
|
}
|
|
}
|
|
|
|
impl ClientApiImpl {
|
|
async fn authorize_bilibili_live_danmaku_access(
|
|
&self,
|
|
actor: &RoomActor,
|
|
target_is_current: impl FnOnce(&synctv_core::models::RoomPlaybackState) -> bool,
|
|
) -> Result<(), ApiError> {
|
|
let room_id = actor.room_id();
|
|
let state = self
|
|
.room_service
|
|
.get_playback_state(&room_id)
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
require_current_bilibili_live_danmaku_target(target_is_current(&state))
|
|
}
|
|
|
|
pub async fn watch_bilibili_live_danmaku_for_actor(
|
|
&self,
|
|
actor: &RoomActor,
|
|
req: synctv_proto::playback_provider::bilibili::WatchBilibiliLiveDanmakuRequest,
|
|
request_control: Option<&synctv_core::provider::ExecutionControl>,
|
|
) -> Result<BilibiliLiveDanmakuStream, ApiError> {
|
|
crate::impls::validate_proto_request(&req)?;
|
|
match req.target.ok_or_else(|| {
|
|
ApiError::InvalidInput("Bilibili live danmaku target is required".to_string())
|
|
})? {
|
|
synctv_proto::playback_provider::bilibili::watch_bilibili_live_danmaku_request::Target::MediaId(
|
|
media_id,
|
|
) => {
|
|
self.watch_bilibili_media_live_danmaku_for_actor(actor, media_id, request_control)
|
|
.await
|
|
}
|
|
synctv_proto::playback_provider::bilibili::watch_bilibili_live_danmaku_request::Target::Dynamic(dynamic) => {
|
|
self.watch_bilibili_dynamic_live_danmaku_for_actor(actor, dynamic, request_control)
|
|
.await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn watch_bilibili_media_live_danmaku_for_actor(
|
|
&self,
|
|
actor: &RoomActor,
|
|
media_id: String,
|
|
request_control: Option<&synctv_core::provider::ExecutionControl>,
|
|
) -> Result<BilibiliLiveDanmakuStream, ApiError> {
|
|
let room_id = actor.room_id();
|
|
let media_id = crate::impls::proto_validated_media_id(media_id, &self.public_id_codec)?;
|
|
self.authorize_bilibili_live_danmaku_access(actor, |state| {
|
|
guest_is_current_bilibili_live_media(state, &media_id)
|
|
})
|
|
.await?;
|
|
let provider_actor = super::provider_actor_for_viewer(actor.user_id());
|
|
let media = self
|
|
.room_service
|
|
.media_service()
|
|
.get_room_media(&room_id, &media_id)
|
|
.await
|
|
.map_err(ApiError::from)?
|
|
.ok_or_else(|| ApiError::NotFound("Media not found".to_string()))?;
|
|
self.room_service
|
|
.ensure_client_usable_media(&media)
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
if media.source_provider != synctv_core::models::SourceProvider::Bilibili {
|
|
return Err(ApiError::InvalidInput(
|
|
"Bilibili live danmaku requires Bilibili media".to_string(),
|
|
));
|
|
}
|
|
|
|
let provider = self
|
|
.room_service
|
|
.media_service()
|
|
.providers_manager()
|
|
.resolve_provider(
|
|
synctv_core::models::SourceProvider::Bilibili,
|
|
media.provider_instance_name.as_deref(),
|
|
)
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
let live_danmaku_provider =
|
|
provider
|
|
.as_bilibili_live_danmaku_provider()
|
|
.ok_or_else(|| {
|
|
ApiError::ServiceUnavailable(
|
|
"Bilibili provider does not expose live danmaku".to_string(),
|
|
)
|
|
})?;
|
|
let ctx = self.attach_provider_store(
|
|
self.build_provider_context(
|
|
provider_actor,
|
|
media.creator_id.as_ref(),
|
|
&room_id,
|
|
Some(media.id),
|
|
media.provider_instance_name.as_deref(),
|
|
None,
|
|
request_control,
|
|
)?,
|
|
provider.as_ref(),
|
|
);
|
|
let stream = live_danmaku_provider
|
|
.watch_bilibili_live_danmaku(&ctx, &media.source_config)
|
|
.await
|
|
.map_err(ApiError::from)?
|
|
.map(|event| {
|
|
event
|
|
.map(live_danmaku_event_to_proto)
|
|
.map_err(ApiError::from)
|
|
});
|
|
Ok(Box::pin(stream))
|
|
}
|
|
|
|
async fn watch_bilibili_dynamic_live_danmaku_for_actor(
|
|
&self,
|
|
actor: &RoomActor,
|
|
req: synctv_proto::playback_provider::bilibili::BilibiliDynamicLiveDanmakuTarget,
|
|
request_control: Option<&synctv_core::provider::ExecutionControl>,
|
|
) -> Result<BilibiliLiveDanmakuStream, ApiError> {
|
|
crate::impls::validate_proto_request(&req)?;
|
|
let room_id = actor.room_id();
|
|
let playlist_id =
|
|
crate::impls::proto_validated_playlist_id(&req.playlist_id, &self.public_id_codec)?;
|
|
let target = synctv_core::models::ProviderTarget::bilibili_live(req.live_room_id);
|
|
self.authorize_bilibili_live_danmaku_access(actor, |state| {
|
|
guest_is_current_bilibili_dynamic_live(state, &playlist_id, &target)
|
|
})
|
|
.await?;
|
|
let provider_actor = super::provider_actor_for_viewer(actor.user_id());
|
|
let playlist = self
|
|
.room_service
|
|
.playlist_service()
|
|
.get_room_playlist(&room_id, &playlist_id)
|
|
.await
|
|
.map_err(ApiError::from)?
|
|
.ok_or_else(|| ApiError::NotFound("Playlist not found".to_string()))?;
|
|
self.room_service
|
|
.ensure_client_usable_playlist(&playlist)
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
if !playlist.is_dynamic()
|
|
|| playlist.source_provider != Some(synctv_core::models::SourceProvider::Bilibili)
|
|
{
|
|
return Err(ApiError::InvalidInput(
|
|
"Bilibili live danmaku requires a Bilibili dynamic playlist".to_string(),
|
|
));
|
|
}
|
|
|
|
let item = self
|
|
.room_service
|
|
.media_service()
|
|
.resolve_dynamic_playlist_item(room_id, provider_actor, &playlist_id, &target)
|
|
.await
|
|
.map_err(ApiError::from)?
|
|
.ok_or_else(|| ApiError::NotFound("Dynamic playlist item not found".to_string()))?;
|
|
if !matches!(
|
|
&item.source_config,
|
|
synctv_core::models::MediaSourceConfig::Bilibili(
|
|
synctv_core::models::BilibiliMediaSourceConfig::Live(_)
|
|
)
|
|
) {
|
|
return Err(ApiError::InvalidInput(
|
|
"Dynamic playlist item is not a Bilibili live source".to_string(),
|
|
));
|
|
}
|
|
|
|
let provider = self
|
|
.room_service
|
|
.media_service()
|
|
.providers_manager()
|
|
.resolve_provider(
|
|
synctv_core::models::SourceProvider::Bilibili,
|
|
playlist.provider_instance_name.as_deref(),
|
|
)
|
|
.await
|
|
.map_err(ApiError::from)?;
|
|
let live_danmaku_provider =
|
|
provider
|
|
.as_bilibili_live_danmaku_provider()
|
|
.ok_or_else(|| {
|
|
ApiError::ServiceUnavailable(
|
|
"Bilibili provider does not expose live danmaku".to_string(),
|
|
)
|
|
})?;
|
|
let ctx = self.attach_provider_store(
|
|
self.build_provider_context(
|
|
provider_actor,
|
|
playlist.creator_id.as_ref(),
|
|
&room_id,
|
|
None,
|
|
playlist.provider_instance_name.as_deref(),
|
|
None,
|
|
request_control,
|
|
)?
|
|
.with_playlist_id(playlist_id),
|
|
provider.as_ref(),
|
|
);
|
|
let stream = live_danmaku_provider
|
|
.watch_bilibili_live_danmaku(&ctx, &item.source_config)
|
|
.await
|
|
.map_err(ApiError::from)?
|
|
.map(|event| {
|
|
event
|
|
.map(live_danmaku_event_to_proto)
|
|
.map_err(ApiError::from)
|
|
});
|
|
Ok(Box::pin(stream))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
guest_is_current_bilibili_dynamic_live, guest_is_current_bilibili_live_media,
|
|
require_current_bilibili_live_danmaku_target,
|
|
};
|
|
|
|
#[test]
|
|
fn live_danmaku_requires_the_current_playback_target() {
|
|
assert!(require_current_bilibili_live_danmaku_target(true).is_ok());
|
|
assert!(matches!(
|
|
require_current_bilibili_live_danmaku_target(false),
|
|
Err(crate::impls::ApiError::Authorization(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn live_danmaku_target_matching_covers_media_and_dynamic_playback() {
|
|
let room_id = synctv_core::models::RoomId::expect_positive(1);
|
|
let media_id = synctv_core::models::MediaId::expect_positive(2);
|
|
let playlist_id = synctv_core::models::PlaylistId::expect_positive(3);
|
|
let target = synctv_core::models::ProviderTarget::bilibili_live(21_292_831);
|
|
let mut state = synctv_core::models::RoomPlaybackState::new(room_id);
|
|
|
|
state.playing_media_id = Some(media_id);
|
|
assert!(guest_is_current_bilibili_live_media(&state, &media_id));
|
|
assert!(!guest_is_current_bilibili_live_media(
|
|
&state,
|
|
&synctv_core::models::MediaId::expect_positive(4),
|
|
));
|
|
|
|
state.playing_media_id = None;
|
|
state.playing_playlist_id = Some(playlist_id);
|
|
state.target = Some(target.clone());
|
|
assert!(guest_is_current_bilibili_dynamic_live(
|
|
&state,
|
|
&playlist_id,
|
|
&target,
|
|
));
|
|
assert!(!guest_is_current_bilibili_dynamic_live(
|
|
&state,
|
|
&playlist_id,
|
|
&synctv_core::models::ProviderTarget::bilibili_live(2),
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_live_danmaku_request_requires_a_live_room_id() {
|
|
let request = synctv_proto::playback_provider::bilibili::BilibiliDynamicLiveDanmakuTarget {
|
|
playlist_id: "pl_abc123".to_string(),
|
|
live_room_id: 0,
|
|
};
|
|
|
|
assert!(crate::impls::validate_proto_request(&request).is_err());
|
|
}
|
|
}
|