feat: implement transcode settings repository and integrate with local-files provider

This commit is contained in:
2026-03-16 04:24:39 +01:00
parent 50df852416
commit 9d792249c9
12 changed files with 269 additions and 177 deletions

View File

@@ -17,7 +17,7 @@ use domain::{ChannelRepository, DomainError, DomainEvent, ScheduleEngineService}
/// Per-channel poller state.
#[derive(Debug)]
struct ChannelPollState {
pub struct ChannelPollState {
/// ID of the last slot we saw as current (None = no signal).
last_slot_id: Option<Uuid>,
/// Wall-clock instant of the last poll for this channel.
@@ -80,13 +80,11 @@ pub(crate) async fn poll_tick(
// Find the current slot
let current_slot_id = match schedule_engine.get_active_schedule(channel.id, now).await {
Ok(Some(schedule)) => {
schedule
.slots
.iter()
.find(|s| s.start_at <= now && now < s.end_at)
.map(|s| s.id)
}
Ok(Some(schedule)) => schedule
.slots
.iter()
.find(|s| s.start_at <= now && now < s.end_at)
.map(|s| s.id),
Ok(None) => None,
Err(DomainError::NoActiveSchedule(_)) => None,
Err(DomainError::ChannelNotFound(_)) => {
@@ -109,7 +107,9 @@ pub(crate) async fn poll_tick(
// State changed — emit appropriate event
match &current_slot_id {
Some(slot_id) => {
if let Ok(Some(schedule)) = schedule_engine.get_active_schedule(channel.id, now).await {
if let Ok(Some(schedule)) =
schedule_engine.get_active_schedule(channel.id, now).await
{
if let Some(slot) = schedule.slots.iter().find(|s| s.id == *slot_id).cloned() {
let _ = event_tx.send(DomainEvent::BroadcastTransition {
channel_id: channel.id,
@@ -137,12 +137,12 @@ mod tests {
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use domain::value_objects::{ChannelId, ContentType, UserId};
use domain::{
Channel, ChannelRepository, Collection, DomainResult, GeneratedSchedule, IProviderRegistry,
MediaFilter, MediaItem, MediaItemId, PlaybackRecord, ProviderCapabilities,
ScheduleEngineService, ScheduleRepository, SeriesSummary, StreamQuality, StreamingProtocol,
ScheduleEngineService, ScheduleRepository, SeriesSummary, StreamQuality,
};
use domain::value_objects::{ChannelId, ContentType, UserId};
use tokio::sync::broadcast;
use uuid::Uuid;
@@ -188,14 +188,20 @@ mod tests {
) -> DomainResult<Option<GeneratedSchedule>> {
Ok(self.active.clone())
}
async fn find_latest(&self, _channel_id: ChannelId) -> DomainResult<Option<GeneratedSchedule>> {
async fn find_latest(
&self,
_channel_id: ChannelId,
) -> DomainResult<Option<GeneratedSchedule>> {
Ok(self.active.clone())
}
async fn save(&self, schedule: &GeneratedSchedule) -> DomainResult<()> {
self.saved.lock().unwrap().push(schedule.clone());
Ok(())
}
async fn find_playback_history(&self, _channel_id: ChannelId) -> DomainResult<Vec<PlaybackRecord>> {
async fn find_playback_history(
&self,
_channel_id: ChannelId,
) -> DomainResult<Vec<PlaybackRecord>> {
Ok(vec![])
}
async fn save_playback_record(&self, _record: &PlaybackRecord) -> DomainResult<()> {
@@ -207,13 +213,21 @@ mod tests {
#[async_trait]
impl IProviderRegistry for MockRegistry {
async fn fetch_items(&self, _provider_id: &str, _filter: &MediaFilter) -> DomainResult<Vec<MediaItem>> {
async fn fetch_items(
&self,
_provider_id: &str,
_filter: &MediaFilter,
) -> DomainResult<Vec<MediaItem>> {
Ok(vec![])
}
async fn fetch_by_id(&self, _item_id: &MediaItemId) -> DomainResult<Option<MediaItem>> {
Ok(None)
}
async fn get_stream_url(&self, _item_id: &MediaItemId, _quality: &StreamQuality) -> DomainResult<String> {
async fn get_stream_url(
&self,
_item_id: &MediaItemId,
_quality: &StreamQuality,
) -> DomainResult<String> {
unimplemented!()
}
fn provider_ids(&self) -> Vec<String> {
@@ -228,10 +242,18 @@ mod tests {
async fn list_collections(&self, _provider_id: &str) -> DomainResult<Vec<Collection>> {
unimplemented!()
}
async fn list_series(&self, _provider_id: &str, _collection_id: Option<&str>) -> DomainResult<Vec<SeriesSummary>> {
async fn list_series(
&self,
_provider_id: &str,
_collection_id: Option<&str>,
) -> DomainResult<Vec<SeriesSummary>> {
unimplemented!()
}
async fn list_genres(&self, _provider_id: &str, _content_type: Option<&ContentType>) -> DomainResult<Vec<String>> {
async fn list_genres(
&self,
_provider_id: &str,
_content_type: Option<&ContentType>,
) -> DomainResult<Vec<String>> {
unimplemented!()
}
}
@@ -318,7 +340,10 @@ mod tests {
let event = event_rx.try_recv().expect("expected an event");
match event {
DomainEvent::BroadcastTransition { channel_id: cid, slot: s } => {
DomainEvent::BroadcastTransition {
channel_id: cid,
slot: s,
} => {
assert_eq!(cid, channel_id);
assert_eq!(s.id, slot_id);
}
@@ -388,11 +413,18 @@ mod tests {
async fn find_latest(&self, _: ChannelId) -> DomainResult<Option<GeneratedSchedule>> {
Ok(None)
}
async fn save(&self, _: &GeneratedSchedule) -> DomainResult<()> { Ok(()) }
async fn find_playback_history(&self, _: ChannelId) -> DomainResult<Vec<PlaybackRecord>> {
async fn save(&self, _: &GeneratedSchedule) -> DomainResult<()> {
Ok(())
}
async fn find_playback_history(
&self,
_: ChannelId,
) -> DomainResult<Vec<PlaybackRecord>> {
Ok(vec![])
}
async fn save_playback_record(&self, _: &PlaybackRecord) -> DomainResult<()> { Ok(()) }
async fn save_playback_record(&self, _: &PlaybackRecord) -> DomainResult<()> {
Ok(())
}
}
let now = Utc::now();