Background-audio handoff for video + repository/player refactor

Hand video playback off to a native audio-only stream when the app is
backgrounded or locked, with no on-device video decode (UR-040). Adds
player_enter/exit_background_audio commands, an audio-only stream URL
for video items across the repository layer, and the frontend handoff
state machine wired into VideoPlayer. Includes accompanying
repository/offline/player refactoring and regenerates the traceability
matrix.
This commit is contained in:
2026-07-22 21:52:07 +02:00
parent 4e6ab017d4
commit 3fbf6afdbc
72 changed files with 6728 additions and 2338 deletions
+397 -90
View File
@@ -42,8 +42,8 @@ pub use mpv_backend::MpvBackend;
#[cfg(target_os = "android")]
pub use android::{
MediaCommandHandler, RemoteVolumeHandler, enable_remote_volume, disable_remote_volume,
set_media_command_handler, set_remote_volume_handler, get_detected_codecs,
disable_remote_volume, enable_remote_volume, get_detected_codecs, set_media_command_handler,
set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
};
/// Metadata for the lockscreen / media notification.
@@ -53,6 +53,9 @@ pub use android::{
/// poller fills this in from the remote Jellyfin session and pushes it to the
/// notification so the lockscreen stays in sync while casting.
#[derive(Debug, Clone)]
// Fields are read only by the Android MediaSession bridge; on other platforms
// `update_lockscreen_metadata` is a no-op, so they're constructed but unread.
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub struct LockscreenMetadata {
pub title: String,
pub artist: String,
@@ -84,9 +87,11 @@ use std::time::Duration;
use tokio::sync::Mutex as TokioMutex;
use crate::jellyfin::JellyfinClient;
use crate::settings::AudioSettings;
use crate::playback_reporting::{
EventThrottler, PlaybackContext, PlaybackOperation, PlaybackReporter,
};
use crate::repository::MediaRepository;
use crate::playback_reporting::{PlaybackReporter, EventThrottler, PlaybackOperation, PlaybackContext};
use crate::settings::AudioSettings;
/// Central player controller that coordinates playback
pub struct PlayerController {
@@ -157,7 +162,10 @@ impl PlayerController {
pub fn set_jellyfin_client(&self, client: Option<JellyfinClient>) {
let mut jellyfin = self.jellyfin_client.lock_safe();
*jellyfin = client;
log::info!("[PlayerController] Jellyfin client configured: {}", jellyfin.is_some());
log::info!(
"[PlayerController] Jellyfin client configured: {}",
jellyfin.is_some()
);
}
/// Get a reference to the Jellyfin client (for remote session control)
@@ -180,7 +188,10 @@ impl PlayerController {
pub async fn set_playback_reporter(&self, reporter: Option<PlaybackReporter>) {
let mut reporter_guard = self.playback_reporter.lock().await;
*reporter_guard = reporter;
log::info!("[PlayerController] Playback reporter configured: {}", reporter_guard.is_some());
log::info!(
"[PlayerController] Playback reporter configured: {}",
reporter_guard.is_some()
);
}
/// Get a reference to the playback reporter (for backend position updates)
@@ -219,7 +230,10 @@ impl PlayerController {
let mut count = self.autoplay_episode_count.lock_safe();
*count += 1;
debug!("[PlayerController] Autoplay episode count: {}/{}", *count, max);
debug!(
"[PlayerController] Autoplay episode count: {}/{}",
*count, max
);
*count >= max
}
@@ -228,7 +242,10 @@ impl PlayerController {
fn reset_autoplay_count(&self) {
let mut count = self.autoplay_episode_count.lock_safe();
if *count > 0 {
debug!("[PlayerController] Resetting autoplay episode counter (was {})", *count);
debug!(
"[PlayerController] Resetting autoplay episode counter (was {})",
*count
);
}
*count = 0;
}
@@ -259,7 +276,10 @@ impl PlayerController {
/// item, but MPV must not start a redundant decode for it.
#[cfg(target_os = "linux")]
pub fn set_current_item(&self, item: MediaItem) -> Result<(), PlayerError> {
debug!("[PlayerController] set_current_item (no backend load): {}", item.title);
debug!(
"[PlayerController] set_current_item (no backend load): {}",
item.title
);
self.reset_autoplay_count();
@@ -446,7 +466,9 @@ impl PlayerController {
// Get current playback info before stopping
let jellyfin_id = {
let queue = self.queue.lock_safe();
queue.current().and_then(|item| item.jellyfin_id().map(|s| s.to_string()))
queue
.current()
.and_then(|item| item.jellyfin_id().map(|s| s.to_string()))
};
let position_ticks = {
@@ -522,7 +544,10 @@ impl PlayerController {
queue.next().cloned()
};
debug!("[PlayerController] next: {:?}", next_item.as_ref().map(|i| &i.title));
debug!(
"[PlayerController] next: {:?}",
next_item.as_ref().map(|i| &i.title)
);
if let Some(item) = next_item {
self.load_and_play(&item)
@@ -554,7 +579,10 @@ impl PlayerController {
queue.previous().cloned()
};
debug!("[PlayerController] previous: {:?}", prev_item.as_ref().map(|i| &i.title));
debug!(
"[PlayerController] previous: {:?}",
prev_item.as_ref().map(|i| &i.title)
);
if let Some(item) = prev_item {
self.load_and_play(&item)
@@ -707,7 +735,9 @@ impl PlayerController {
timer.update_remaining_seconds();
// Time-based timer expired: stop playback
if matches!(timer.mode, SleepTimerMode::Time { .. }) && timer.remaining_seconds == 0 {
if matches!(timer.mode, SleepTimerMode::Time { .. })
&& timer.remaining_seconds == 0
{
debug!("[SleepTimer] Time-based timer expired, stopping playback");
timer.cancel();
@@ -843,7 +873,10 @@ impl PlayerController {
// Check why playback ended
let end_reason = self.take_end_reason();
debug!("[PlayerController] on_playback_ended: end_reason={:?}", end_reason);
debug!(
"[PlayerController] on_playback_ended: end_reason={:?}",
end_reason
);
// Only proceed with autoplay logic if track finished naturally
match end_reason {
@@ -907,8 +940,8 @@ impl PlayerController {
}
SleepTimerMode::Episodes { .. } => {
// Only count TV episodes (not audio tracks or movies)
let is_episode = current.media_type == MediaType::Video
&& self.is_episode_item(&current).await;
let is_episode =
current.media_type == MediaType::Video && self.is_episode_item(&current).await;
if is_episode {
let should_stop = self.sleep_timer.lock_safe().decrement_episode();
@@ -936,7 +969,10 @@ impl PlayerController {
match self.fetch_next_episode_for_item(jellyfin_id, repo).await {
Ok(next) => next,
Err(e) => {
warn!("[PlayerController] Next-episode lookup failed for {}: {}", jellyfin_id, e);
warn!(
"[PlayerController] Next-episode lookup failed for {}: {}",
jellyfin_id, e
);
None
}
}
@@ -950,11 +986,14 @@ impl PlayerController {
// Check if auto-play episode limit is reached
let limit_reached = self.increment_autoplay_count();
if limit_reached {
debug!("[PlayerController] Auto-play episode limit reached ({} episodes)", settings.max_episodes);
debug!(
"[PlayerController] Auto-play episode limit reached ({} episodes)",
settings.max_episodes
);
}
return Ok(AutoplayDecision::ShowNextEpisodePopup {
current_episode: next_ep.0, // Repository MediaItem
current_episode: next_ep.0, // Repository MediaItem
next_episode: next_ep.1,
countdown_seconds: settings.countdown_seconds,
auto_advance: settings.enabled && !limit_reached,
@@ -993,10 +1032,16 @@ impl PlayerController {
// Clear any stale end_reason (e.g., UserStop from stopping audio before video)
let stale_reason = self.take_end_reason();
if stale_reason.is_some() {
debug!("[PlayerController] Cleared stale end_reason for video: {:?}", stale_reason);
debug!(
"[PlayerController] Cleared stale end_reason for video: {:?}",
stale_reason
);
}
log::info!("[PlayerController] on_video_playback_ended: item_id={}", item_id);
log::info!(
"[PlayerController] on_video_playback_ended: item_id={}",
item_id
);
// Check sleep timer state
let timer_mode = {
@@ -1035,7 +1080,10 @@ impl PlayerController {
let next_ep_result = match self.fetch_next_episode_for_item(item_id, &repo).await {
Ok(next) => next,
Err(e) => {
warn!("[PlayerController] Next-episode lookup failed for {}: {}", item_id, e);
warn!(
"[PlayerController] Next-episode lookup failed for {}: {}",
item_id, e
);
None
}
};
@@ -1044,7 +1092,10 @@ impl PlayerController {
let limit_reached = self.increment_autoplay_count();
if limit_reached {
debug!("[PlayerController] Auto-play episode limit reached ({} episodes)", settings.max_episodes);
debug!(
"[PlayerController] Auto-play episode limit reached ({} episodes)",
settings.max_episodes
);
}
return Ok(AutoplayDecision::ShowNextEpisodePopup {
@@ -1077,11 +1128,18 @@ impl PlayerController {
&self,
item_id: &str,
repo: &Arc<dyn crate::repository::MediaRepository>,
) -> Result<Option<(crate::repository::types::MediaItem, crate::repository::types::MediaItem)>, String> {
) -> Result<
Option<(
crate::repository::types::MediaItem,
crate::repository::types::MediaItem,
)>,
String,
> {
use crate::repository::types::GetItemsOptions;
// Get the current item details from repository
let current_repo_item = repo.get_item(item_id)
let current_repo_item = repo
.get_item(item_id)
.await
.map_err(|e| format!("Failed to get current item: {}", e))?;
@@ -1089,7 +1147,9 @@ impl PlayerController {
let season_id = match &current_repo_item.season_id {
Some(sid) => sid.clone(),
None => {
log::info!("[PlayerController] Current item has no season_id, cannot find next episode");
log::info!(
"[PlayerController] Current item has no season_id, cannot find next episode"
);
return Ok(None);
}
};
@@ -1103,7 +1163,8 @@ impl PlayerController {
..Default::default()
};
let result = repo.get_items(&season_id, Some(options))
let result = repo
.get_items(&season_id, Some(options))
.await
.map_err(|e| format!("Failed to fetch season episodes: {}", e))?;
@@ -1111,26 +1172,45 @@ impl PlayerController {
// (offline repo ignores sort_by and sorts by sort_name instead)
let mut episodes = result.items;
episodes.sort_by_key(|e| e.index_number.unwrap_or(i32::MAX));
log::info!("[PlayerController] Season has {} episodes, looking for next after {}", episodes.len(), current_repo_item.id);
log::info!(
"[PlayerController] Season has {} episodes, looking for next after {}",
episodes.len(),
current_repo_item.id
);
// Find the current episode by ID and return the next one
if let Some(current_idx) = episodes.iter().position(|e| e.id == current_repo_item.id) {
if current_idx + 1 < episodes.len() {
let next = &episodes[current_idx + 1];
log::info!("[PlayerController] Found next episode: {} (index {})", next.name, current_idx + 1);
log::info!(
"[PlayerController] Found next episode: {} (index {})",
next.name,
current_idx + 1
);
return Ok(Some((current_repo_item, next.clone())));
} else {
log::info!("[PlayerController] Current episode is the last in the season");
}
} else {
log::info!("[PlayerController] Current episode not found in season episodes (ids: {:?})", episodes.iter().map(|e| e.id.as_str()).take(20).collect::<Vec<_>>());
log::info!(
"[PlayerController] Current episode not found in season episodes (ids: {:?})",
episodes
.iter()
.map(|e| e.id.as_str())
.take(20)
.collect::<Vec<_>>()
);
}
Ok(None)
}
/// Start autoplay countdown thread
pub fn start_autoplay_countdown(&self, _next_item: crate::repository::types::MediaItem, countdown_seconds: u32) {
pub fn start_autoplay_countdown(
&self,
_next_item: crate::repository::types::MediaItem,
countdown_seconds: u32,
) {
// Create cancellation flag
let cancel_flag = Arc::new(Mutex::new(false));
*self.countdown_cancel.lock_safe() = Some(cancel_flag.clone());
@@ -1169,7 +1249,11 @@ impl Default for PlayerController {
fn default() -> Self {
let playback_reporter = Arc::new(TokioMutex::new(None));
let position_throttler = Arc::new(EventThrottler::new());
Self::new(Box::new(NullBackend::new()), playback_reporter, position_throttler)
Self::new(
Box::new(NullBackend::new()),
playback_reporter,
position_throttler,
)
}
}
@@ -1332,8 +1416,16 @@ mod tests {
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 5, "Queue should have 5 items");
assert_eq!(queue_lock.current_index(), Some(0), "Should start at index 0");
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Current item should be item_0");
assert_eq!(
queue_lock.current_index(),
Some(0),
"Should start at index 0"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_0",
"Current item should be item_0"
);
}
// Skip to next track
@@ -1343,15 +1435,35 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after skip");
assert_eq!(queue_lock.current_index(), Some(1), "Index should advance to 1");
assert_eq!(queue_lock.current().unwrap().id, "item_1", "Current item should be item_1");
assert_eq!(
queue_lock.items().len(),
5,
"Queue should still have 5 items after skip"
);
assert_eq!(
queue_lock.current_index(),
Some(1),
"Index should advance to 1"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_1",
"Current item should be item_1"
);
// Verify all original items are still present
let current_items = queue_lock.items();
for (i, original) in items_clone.iter().enumerate() {
assert_eq!(current_items[i].id, original.id, "Item {} should still be in queue", i);
assert_eq!(current_items[i].title, original.title, "Item {} title should be unchanged", i);
assert_eq!(
current_items[i].id, original.id,
"Item {} should still be in queue",
i
);
assert_eq!(
current_items[i].title, original.title,
"Item {} title should be unchanged",
i
);
}
}
@@ -1362,9 +1474,21 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after second skip");
assert_eq!(queue_lock.current_index(), Some(2), "Index should advance to 2");
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
assert_eq!(
queue_lock.items().len(),
5,
"Queue should still have 5 items after second skip"
);
assert_eq!(
queue_lock.current_index(),
Some(2),
"Index should advance to 2"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_2",
"Current item should be item_2"
);
}
// Skip multiple times to reach the end
@@ -1375,9 +1499,21 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items at end");
assert_eq!(queue_lock.current_index(), Some(4), "Index should be at last item (4)");
assert_eq!(queue_lock.current().unwrap().id, "item_4", "Current item should be item_4");
assert_eq!(
queue_lock.items().len(),
5,
"Queue should still have 5 items at end"
);
assert_eq!(
queue_lock.current_index(),
Some(4),
"Index should be at last item (4)"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_4",
"Current item should be item_4"
);
}
}
@@ -1397,7 +1533,11 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.current_index(), Some(2), "Should be at last item");
assert_eq!(
queue_lock.current_index(),
Some(2),
"Should be at last item"
);
}
// Try to skip past the end (without repeat mode)
@@ -1408,7 +1548,11 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items after skip at end");
assert_eq!(
queue_lock.items().len(),
3,
"Queue should still have 3 items after skip at end"
);
// When we skip past the end, the queue index should stay at the last item
// or become None (depending on implementation)
// The key is the queue items themselves should be preserved
@@ -1437,9 +1581,21 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 3, "Queue should still have 3 items");
assert_eq!(queue_lock.current_index(), Some(0), "Should wrap to index 0");
assert_eq!(queue_lock.current().unwrap().id, "item_0", "Should be back at item_0");
assert_eq!(
queue_lock.items().len(),
3,
"Queue should still have 3 items"
);
assert_eq!(
queue_lock.current_index(),
Some(0),
"Should wrap to index 0"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_0",
"Should be back at item_0"
);
}
}
@@ -1456,7 +1612,11 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.current_index(), Some(3), "Should start at index 3");
assert_eq!(
queue_lock.current_index(),
Some(3),
"Should start at index 3"
);
}
// Go to previous track
@@ -1466,14 +1626,30 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.items().len(), 5, "Queue should still have 5 items after previous");
assert_eq!(queue_lock.current_index(), Some(2), "Index should move to 2");
assert_eq!(queue_lock.current().unwrap().id, "item_2", "Current item should be item_2");
assert_eq!(
queue_lock.items().len(),
5,
"Queue should still have 5 items after previous"
);
assert_eq!(
queue_lock.current_index(),
Some(2),
"Index should move to 2"
);
assert_eq!(
queue_lock.current().unwrap().id,
"item_2",
"Current item should be item_2"
);
// Verify all original items are still present
let current_items = queue_lock.items();
for (i, original) in items_clone.iter().enumerate() {
assert_eq!(current_items[i].id, original.id, "Item {} should still be in queue", i);
assert_eq!(
current_items[i].id, original.id,
"Item {} should still be in queue",
i
);
}
}
}
@@ -1491,15 +1667,27 @@ mod tests {
// Seek to 30 seconds
controller.seek(30.0).unwrap();
assert_eq!(controller.position(), 30.0, "Position should be 30 after seeking");
assert_eq!(
controller.position(),
30.0,
"Position should be 30 after seeking"
);
// Seek to 60 seconds
controller.seek(60.0).unwrap();
assert_eq!(controller.position(), 60.0, "Position should be 60 after seeking");
assert_eq!(
controller.position(),
60.0,
"Position should be 60 after seeking"
);
// Seek backward to 15 seconds
controller.seek(15.0).unwrap();
assert_eq!(controller.position(), 15.0, "Position should be 15 after seeking backward");
assert_eq!(
controller.position(),
15.0,
"Position should be 15 after seeking backward"
);
}
#[test]
@@ -1518,10 +1706,17 @@ mod tests {
// Seek while paused
controller.seek(45.0).unwrap();
assert_eq!(controller.position(), 45.0, "Position should update while paused");
assert_eq!(
controller.position(),
45.0,
"Position should update while paused"
);
// Verify still paused after seeking
assert!(controller.state().is_paused(), "Should still be paused after seeking");
assert!(
controller.state().is_paused(),
"Should still be paused after seeking"
);
}
#[test]
@@ -1540,10 +1735,17 @@ mod tests {
// Seek while playing
controller.seek(20.0).unwrap();
assert_eq!(controller.position(), 20.0, "Position should update while playing");
assert_eq!(
controller.position(),
20.0,
"Position should update while playing"
);
// Verify still playing after seeking
assert!(controller.state().is_playing(), "Should still be playing after seeking");
assert!(
controller.state().is_playing(),
"Should still be playing after seeking"
);
}
#[test]
@@ -1558,7 +1760,12 @@ mod tests {
for pos in positions {
controller.seek(pos).unwrap();
assert_eq!(controller.position(), pos, "Position should match after seeking to {}", pos);
assert_eq!(
controller.position(),
pos,
"Position should match after seeking to {}",
pos
);
}
}
@@ -1575,9 +1782,17 @@ mod tests {
{
let queue = controller.queue();
let queue_lock = queue.lock_safe();
assert_eq!(queue_lock.current_index(), Some(1), "Should start at index 1");
assert_eq!(
queue_lock.current_index(),
Some(1),
"Should start at index 1"
);
}
assert_eq!(controller.position(), 42.5, "Should resume at the requested position");
assert_eq!(
controller.position(),
42.5,
"Should resume at the requested position"
);
}
/// A None / near-zero start position starts the track from the beginning.
@@ -1613,7 +1828,11 @@ mod tests {
// Seek back to zero
controller.seek(0.0).unwrap();
assert_eq!(controller.position(), 0.0, "Should be able to seek to position 0");
assert_eq!(
controller.position(),
0.0,
"Should be able to seek to position 0"
);
}
// Autoplay decision tests
@@ -1990,7 +2209,10 @@ mod tests {
total_record_count: self.episodes.len(),
})
}
async fn get_item(&self, item_id: &str) -> Result<repo_types::MediaItem, repo_types::RepoError> {
async fn get_item(
&self,
item_id: &str,
) -> Result<repo_types::MediaItem, repo_types::RepoError> {
self.episodes
.iter()
.find(|e| e.id == item_id)
@@ -1999,55 +2221,109 @@ mod tests {
message: format!("{} not found", item_id),
})
}
async fn get_latest_items(&self, _: &str, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_latest_items(
&self,
_: &str,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_resume_items(&self, _: Option<&str>, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_resume_items(
&self,
_: Option<&str>,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_next_up_episodes(&self, _: Option<&str>, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_next_up_episodes(
&self,
_: Option<&str>,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_recently_played_audio(&self, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_recently_played_audio(
&self,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_rediscover_albums(&self, _: Option<&str>, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_rediscover_albums(
&self,
_: Option<&str>,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_resume_movies(&self, _: Option<usize>) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_resume_movies(
&self,
_: Option<usize>,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_genres(&self, _: Option<&str>) -> Result<Vec<repo_types::Genre>, repo_types::RepoError> {
async fn get_genres(
&self,
_: Option<&str>,
) -> Result<Vec<repo_types::Genre>, repo_types::RepoError> {
unimplemented!()
}
async fn search(&self, _: &str, _: Option<repo_types::SearchOptions>) -> Result<repo_types::SearchResult, repo_types::RepoError> {
async fn search(
&self,
_: &str,
_: Option<repo_types::SearchOptions>,
) -> Result<repo_types::SearchResult, repo_types::RepoError> {
unimplemented!()
}
async fn get_playback_info(&self, _: &str) -> Result<repo_types::PlaybackInfo, repo_types::RepoError> {
async fn get_playback_info(
&self,
_: &str,
) -> Result<repo_types::PlaybackInfo, repo_types::RepoError> {
unimplemented!()
}
async fn get_audio_stream_url(&self, _: &str) -> Result<String, repo_types::RepoError> {
unimplemented!()
}
async fn get_live_tv_channels(&self) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
async fn get_live_tv_channels(
&self,
) -> Result<Vec<repo_types::MediaItem>, repo_types::RepoError> {
unimplemented!()
}
async fn get_channels(&self) -> Result<repo_types::SearchResult, repo_types::RepoError> {
unimplemented!()
}
async fn open_live_stream(&self, _: &str) -> Result<repo_types::LiveStreamInfo, repo_types::RepoError> {
async fn open_live_stream(
&self,
_: &str,
) -> Result<repo_types::LiveStreamInfo, repo_types::RepoError> {
unimplemented!()
}
async fn report_playback_start(&self, _: &str, _: i64) -> Result<(), repo_types::RepoError> {
async fn report_playback_start(
&self,
_: &str,
_: i64,
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn report_playback_progress(&self, _: &str, _: i64) -> Result<(), repo_types::RepoError> {
async fn report_playback_progress(
&self,
_: &str,
_: i64,
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn report_playback_stopped(&self, _: &str, _: i64) -> Result<(), repo_types::RepoError> {
async fn report_playback_stopped(
&self,
_: &str,
_: i64,
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
fn get_image_url(&self, _: &str, _: repo_types::ImageType, _: Option<repo_types::ImageOptions>) -> String {
fn get_image_url(
&self,
_: &str,
_: repo_types::ImageType,
_: Option<repo_types::ImageOptions>,
) -> String {
unimplemented!()
}
fn get_subtitle_url(&self, _: &str, _: &str, _: i32, _: &str) -> String {
@@ -2062,16 +2338,31 @@ mod tests {
async fn unmark_favorite(&self, _: &str) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn get_person(&self, _: &str) -> Result<repo_types::MediaItem, repo_types::RepoError> {
async fn get_person(
&self,
_: &str,
) -> Result<repo_types::MediaItem, repo_types::RepoError> {
unimplemented!()
}
async fn get_items_by_person(&self, _: &str, _: Option<repo_types::GetItemsOptions>) -> Result<repo_types::SearchResult, repo_types::RepoError> {
async fn get_items_by_person(
&self,
_: &str,
_: Option<repo_types::GetItemsOptions>,
) -> Result<repo_types::SearchResult, repo_types::RepoError> {
unimplemented!()
}
async fn get_similar_items(&self, _: &str, _: Option<usize>) -> Result<repo_types::SearchResult, repo_types::RepoError> {
async fn get_similar_items(
&self,
_: &str,
_: Option<usize>,
) -> Result<repo_types::SearchResult, repo_types::RepoError> {
unimplemented!()
}
async fn create_playlist(&self, _: &str, _: &[String]) -> Result<repo_types::PlaylistCreatedResult, repo_types::RepoError> {
async fn create_playlist(
&self,
_: &str,
_: &[String],
) -> Result<repo_types::PlaylistCreatedResult, repo_types::RepoError> {
unimplemented!()
}
async fn delete_playlist(&self, _: &str) -> Result<(), repo_types::RepoError> {
@@ -2080,16 +2371,32 @@ mod tests {
async fn rename_playlist(&self, _: &str, _: &str) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn get_playlist_items(&self, _: &str) -> Result<Vec<repo_types::PlaylistEntry>, repo_types::RepoError> {
async fn get_playlist_items(
&self,
_: &str,
) -> Result<Vec<repo_types::PlaylistEntry>, repo_types::RepoError> {
unimplemented!()
}
async fn add_to_playlist(&self, _: &str, _: &[String]) -> Result<(), repo_types::RepoError> {
async fn add_to_playlist(
&self,
_: &str,
_: &[String],
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn remove_from_playlist(&self, _: &str, _: &[String]) -> Result<(), repo_types::RepoError> {
async fn remove_from_playlist(
&self,
_: &str,
_: &[String],
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
async fn move_playlist_item(&self, _: &str, _: &str, _: u32) -> Result<(), repo_types::RepoError> {
async fn move_playlist_item(
&self,
_: &str,
_: &str,
_: u32,
) -> Result<(), repo_types::RepoError> {
unimplemented!()
}
}