Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
This commit is contained in:
+135
-24
@@ -770,8 +770,18 @@ impl PlayerController {
|
||||
}
|
||||
|
||||
// For video episodes, fetch next episode and show popup
|
||||
// Note: This path is typically not hit for HTML5 video (which uses on_video_playback_ended).
|
||||
// It's here for the Android ExoPlayer path where video items may be in the backend queue.
|
||||
if current.media_type == MediaType::Video && self.is_episode_item(¤t).await {
|
||||
if let Some(next_ep) = self.fetch_next_episode_for_item(¤t).await? {
|
||||
let repo = self.repository.lock().unwrap().clone();
|
||||
let jellyfin_id = current.jellyfin_id().unwrap_or(¤t.id);
|
||||
let next_ep_result = if let Some(repo) = &repo {
|
||||
self.fetch_next_episode_for_item(jellyfin_id, repo).await?
|
||||
} else {
|
||||
debug!("[PlayerController] No repository available for audio-path episode lookup");
|
||||
None
|
||||
};
|
||||
if let Some(next_ep) = next_ep_result {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
|
||||
// Check if auto-play episode limit is reached
|
||||
@@ -806,6 +816,78 @@ impl PlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle video playback ended from HTML5 video element.
|
||||
///
|
||||
/// HTML5 video plays independently of the Rust backend, so the backend
|
||||
/// queue has no knowledge of the video item. This method bypasses the
|
||||
/// queue lookup and end_reason check, using the provided Jellyfin item ID
|
||||
/// to look up the item and check for next episodes.
|
||||
pub async fn on_video_playback_ended(
|
||||
&self,
|
||||
item_id: &str,
|
||||
repo: Arc<dyn crate::repository::MediaRepository>,
|
||||
) -> Result<AutoplayDecision, String> {
|
||||
// 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] on_video_playback_ended: item_id={}", item_id);
|
||||
|
||||
// Check sleep timer state
|
||||
let timer_mode = {
|
||||
let timer = self.sleep_timer.lock().unwrap();
|
||||
timer.mode.clone()
|
||||
};
|
||||
|
||||
match &timer_mode {
|
||||
SleepTimerMode::Time { end_time } => {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
if now >= *end_time {
|
||||
debug!("[PlayerController] Time-based sleep timer expired at video end");
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
SleepTimerMode::EndOfTrack => {
|
||||
self.sleep_timer.lock().unwrap().cancel();
|
||||
self.emit_sleep_timer_changed();
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
SleepTimerMode::Episodes { .. } => {
|
||||
let should_stop = self.sleep_timer.lock().unwrap().decrement_episode();
|
||||
self.emit_sleep_timer_changed();
|
||||
if should_stop {
|
||||
return Ok(AutoplayDecision::Stop);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Fetch next episode for the video that just ended
|
||||
if let Some(next_ep) = self.fetch_next_episode_for_item(item_id, &repo).await? {
|
||||
let settings = self.autoplay_settings.lock().unwrap().clone();
|
||||
|
||||
let limit_reached = self.increment_autoplay_count();
|
||||
if limit_reached {
|
||||
debug!("[PlayerController] Auto-play episode limit reached ({} episodes)", settings.max_episodes);
|
||||
}
|
||||
|
||||
return Ok(AutoplayDecision::ShowNextEpisodePopup {
|
||||
current_episode: next_ep.0,
|
||||
next_episode: next_ep.1,
|
||||
countdown_seconds: settings.countdown_seconds,
|
||||
auto_advance: settings.enabled && !limit_reached,
|
||||
});
|
||||
}
|
||||
|
||||
// No next episode found
|
||||
debug!("[PlayerController] No next episode found for {}", item_id);
|
||||
Ok(AutoplayDecision::Stop)
|
||||
}
|
||||
|
||||
/// Check if a media item is an episode (has Jellyfin ID to query)
|
||||
async fn is_episode_item(&self, item: &MediaItem) -> bool {
|
||||
// For now, assume video items are episodes
|
||||
@@ -813,34 +895,63 @@ impl PlayerController {
|
||||
item.media_type == MediaType::Video
|
||||
}
|
||||
|
||||
/// Fetch next episode for a series (using Repository)
|
||||
async fn fetch_next_episode_for_item(&self, current: &MediaItem) -> Result<Option<(crate::repository::types::MediaItem, crate::repository::types::MediaItem)>, String> {
|
||||
let repo = self.repository.lock().unwrap().clone();
|
||||
let Some(repo) = repo else {
|
||||
return Ok(None);
|
||||
};
|
||||
/// Fetch next episode for a series by looking up the season's episodes
|
||||
/// sorted by index number and picking the one after the current episode.
|
||||
///
|
||||
/// This is deterministic and doesn't depend on Jellyfin's "Next Up" API
|
||||
/// (which relies on watch history that may not be updated yet due to
|
||||
/// the async nature of playback progress reporting).
|
||||
async fn fetch_next_episode_for_item(
|
||||
&self,
|
||||
item_id: &str,
|
||||
repo: &Arc<dyn crate::repository::MediaRepository>,
|
||||
) -> Result<Option<(crate::repository::types::MediaItem, crate::repository::types::MediaItem)>, String> {
|
||||
use crate::repository::types::GetItemsOptions;
|
||||
|
||||
let jellyfin_id = current.jellyfin_id()
|
||||
.ok_or_else(|| "No Jellyfin ID for current item".to_string())?;
|
||||
|
||||
// First, get the current item details from repository
|
||||
let current_repo_item = repo.get_item(jellyfin_id)
|
||||
// Get the current item details from repository
|
||||
let current_repo_item = repo.get_item(item_id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get current item: {}", e))?;
|
||||
|
||||
let series_id = current_repo_item.series_id.clone()
|
||||
.ok_or_else(|| "Current item is not an episode".to_string())?;
|
||||
|
||||
// Fetch next up episodes for this series
|
||||
let next_episodes = repo.get_next_up_episodes(Some(&series_id), Some(1))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch next episodes: {}", e))?;
|
||||
|
||||
if let Some(next) = next_episodes.first() {
|
||||
// Verify it's not the same episode
|
||||
if next.id != current_repo_item.id {
|
||||
return Ok(Some((current_repo_item, next.clone())));
|
||||
// Need season_id to fetch sibling episodes
|
||||
let season_id = match ¤t_repo_item.season_id {
|
||||
Some(sid) => sid.clone(),
|
||||
None => {
|
||||
debug!("[PlayerController] Current item has no season_id, cannot find next episode");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch all episodes in the season sorted by episode number
|
||||
let options = GetItemsOptions {
|
||||
sort_by: Some("IndexNumber".to_string()),
|
||||
sort_order: Some("Ascending".to_string()),
|
||||
limit: Some(500),
|
||||
include_item_types: Some(vec!["Episode".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = repo.get_items(&season_id, Some(options))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch season episodes: {}", e))?;
|
||||
|
||||
// Sort client-side by index_number to ensure correct ordering
|
||||
// (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));
|
||||
debug!("[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];
|
||||
debug!("[PlayerController] Found next episode: {} (index {})", next.name, current_idx + 1);
|
||||
return Ok(Some((current_repo_item, next.clone())));
|
||||
} else {
|
||||
debug!("[PlayerController] Current episode is the last in the season");
|
||||
}
|
||||
} else {
|
||||
debug!("[PlayerController] Current episode not found in season episodes");
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
|
||||
Reference in New Issue
Block a user