Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(player): add Seek player action #689

Merged
merged 8 commits into from
Nov 18, 2024
176 changes: 128 additions & 48 deletions src/models/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ pub struct Player {
pub seek_history: Vec<SeekLog>,
#[serde(skip_serializing)]
pub skip_gaps: Option<(SkipGapsRequest, Loadable<SkipGapsResponse, CtxError>)>,
/// Enable or disable Seek log collection.
///
/// Default: `false` (Do not collect)
#[serde(default, skip_serializing)]
pub collect_seek_logs: bool,
}

impl<E: Env + 'static> UpdateWithCtx<E> for Player {
Expand Down Expand Up @@ -380,6 +385,73 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
}))
.unchanged()
}
Msg::Action(Action::Player(ActionPlayer::Seek {
time,
duration,
device,
})) => match (&self.selected, &mut self.library_item) {
(
// make sure we have a Selected
Some(_selected),
Some(library_item),
) => {
// We might want to consider whether we want to update the LibraryItem for next video
// like we do for TimeChanged

// update the last_watched
library_item.state.last_watched = Some(E::now());

if self.collect_seek_logs {
// collect seek history
if library_item.r#type == "series" && time < &PLAYER_IGNORE_SEEK_AFTER {
self.seek_history.push(SeekLog {
from: library_item.state.time_offset,
to: *time,
});
}
}
// };
time.clone_into(&mut library_item.state.time_offset);
duration.clone_into(&mut library_item.state.duration);
// No need to check and flag the library item as watched,
// seeking does not update the time_watched!

// Nor there's a need to update removed and temp, this can only happen
// after we mark a LibraryItem as watched! Leave this to TimeChanged

// Update the analytics, we still want to keep the correct time and duration updated
if let Some(analytics_context) = &mut self.analytics_context {
library_item
.state
.video_id
.clone_into(&mut analytics_context.video_id);
analytics_context.time = Some(library_item.state.time_offset);
analytics_context.duration = Some(library_item.state.duration);
analytics_context.device_type = Some(device.to_owned());
analytics_context.device_name = Some(device.to_owned());
analytics_context.player_duration = Some(duration.to_owned());
};

// on seeking we want to make sure we send the correct Trakt events
let trakt_event_effects = match (self.loaded, self.paused) {
(true, Some(true)) => Effects::msg(Msg::Event(Event::TraktPaused {
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
}))
.unchanged(),
(true, Some(false)) => Effects::msg(Msg::Event(Event::TraktPlaying {
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
}))
.unchanged(),
_ => Effects::none(),
};

let push_to_library_effects =
push_to_library::<E>(&mut self.push_library_item_time, library_item);

trakt_event_effects.join(push_to_library_effects)
}
_ => Effects::none().unchanged(),
},
Msg::Action(Action::Player(ActionPlayer::TimeChanged {
time,
duration,
Expand All @@ -396,8 +468,6 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
}),
Some(library_item),
) => {
let seeking = library_item.state.time_offset.abs_diff(*time) > 1000;

// if we've selected a new video (like the next episode)
library_item.state.last_watched = Some(E::now());
if library_item.state.video_id != Some(video_id.to_owned()) {
Expand All @@ -409,29 +479,26 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
library_item.state.time_watched = 0;
library_item.state.flagged_watched = 0;
} else {
// else we have added to the currently selected video/stream
// seek logging
if seeking
&& library_item.r#type == "series"
&& time < &PLAYER_IGNORE_SEEK_AFTER
{
self.seek_history.push(SeekLog {
from: library_item.state.time_offset,
to: *time,
});
}

let time_watched =
1000.min(time.saturating_sub(library_item.state.time_offset));
let time_watched = time.saturating_sub(library_item.state.time_offset);
library_item.state.time_watched =
library_item.state.time_watched.saturating_add(time_watched);
library_item.state.overall_time_watched = library_item
.state
.overall_time_watched
.saturating_add(time_watched);
};
time.clone_into(&mut library_item.state.time_offset);
duration.clone_into(&mut library_item.state.duration);

// if we seek forward, time will be < time_offset
// this is the only thing we can guard against!
//
// for both backward and forward seeking we expect the apps to
// send the right actions and update the times accordingly
// when the state changes (from seeking to playing and vice versa)
if time > &library_item.state.time_offset {
time.clone_into(&mut library_item.state.time_offset);
duration.clone_into(&mut library_item.state.duration);
}

if library_item.state.flagged_watched == 0
&& library_item.state.time_watched as f64
> library_item.state.duration as f64 * WATCHED_THRESHOLD_COEF
Expand All @@ -444,13 +511,16 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
watched_bit_field.set_video(video_id, true);
library_item.state.watched = Some(watched_bit_field.into());
}
};
}

if library_item.temp && library_item.state.times_watched == 0 {
library_item.removed = true;
};
}

if library_item.removed {
library_item.temp = true;
};
}

if let Some(analytics_context) = &mut self.analytics_context {
library_item
.state
Expand All @@ -462,34 +532,8 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
analytics_context.device_name = Some(device.to_owned());
analytics_context.player_duration = Some(duration.to_owned());
};
let trakt_event_effects = if seeking && self.loaded && self.paused.is_some() {
if self.paused.expect("paused is None") {
Effects::msg(Msg::Event(Event::TraktPaused {
context: self
.analytics_context
.as_ref()
.cloned()
.unwrap_or_default(),
}))
.unchanged()
} else {
Effects::msg(Msg::Event(Event::TraktPlaying {
context: self
.analytics_context
.as_ref()
.cloned()
.unwrap_or_default(),
}))
.unchanged()
}
} else {
Effects::none()
};

let push_to_library_effects =
push_to_library::<E>(&mut self.push_library_item_time, library_item);

trakt_event_effects.join(push_to_library_effects)
push_to_library::<E>(&mut self.push_library_item_time, library_item)
}
_ => Effects::none().unchanged(),
},
Expand Down Expand Up @@ -529,6 +573,42 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
};
trakt_event_effects.join(update_library_item_effects)
}
Msg::Action(Action::Player(ActionPlayer::PausedChanged { paused }))
if self.selected.is_some() =>
{
self.paused = Some(*paused);
let trakt_event_effects = if !self.loaded {
self.loaded = true;
Effects::msg(Msg::Event(Event::PlayerPlaying {
load_time: self
.load_time
.map(|load_time| {
E::now().timestamp_millis() - load_time.timestamp_millis()
})
.unwrap_or(-1),
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
}))
.unchanged()
} else if *paused {
Effects::msg(Msg::Event(Event::TraktPaused {
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
}))
.unchanged()
} else {
Effects::msg(Msg::Event(Event::TraktPlaying {
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
}))
.unchanged()
};
let update_library_item_effects = match &self.library_item {
Some(library_item) => Effects::msg(Msg::Internal(Internal::UpdateLibraryItem(
library_item.to_owned(),
)))
.unchanged(),
_ => Effects::none().unchanged(),
};
trakt_event_effects.join(update_library_item_effects)
}
Msg::Action(Action::Player(ActionPlayer::NextVideo)) => {
let seek_history_effects = seek_update::<E>(
self.selected.as_ref(),
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/msg/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,23 @@ pub enum ActionPlayer {
StreamStateChanged {
state: StreamItemState,
},
/// Seek performed by the user when using the seekbar or
/// the shortcuts for seeking.
///
/// When transitioning from Seek to TimeChanged and vice-versa
/// we need to make sure to update the other accordingly
/// if we have any type of throttling of these events,
/// otherwise we will get wrong `LibraryItem.state.time_offset`!
Seek {
time: u64,
duration: u64,
device: String,
},
/// A normal playback by the video player
///
/// The time from one TimeChanged action to another can only grow (move forward)
/// and should never go backwards, except when a [`ActionPlayer::Seek`] happen
/// and moves the time backwards.
TimeChanged {
time: u64,
duration: u64,
Expand Down