Skip to content

Commit

Permalink
feat: Player - seek history request
Browse files Browse the repository at this point in the history
Signed-off-by: Lachezar Lechev <[email protected]>
  • Loading branch information
elpiel committed Nov 24, 2023
1 parent a575e8e commit 61302b1
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 12 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ chrono = { version = "0.4", features = ["serde"] }
semver = { version = "1", features = ["serde"] }
base64 = "0.21"
sha1 = "0.10"
sha2 = "0.10"

either = "1.6"
enclose = "1.1"
derivative = "2.2"
Expand Down
3 changes: 3 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ pub const URI_COMPONENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'(')
.remove(b')');

/// In milliseconds
pub const PLAYER_IGNORE_SEEK_AFTER: u64 = 600_000;

pub static BASE64: base64::engine::general_purpose::GeneralPurpose =
base64::engine::general_purpose::STANDARD;

Expand Down
164 changes: 154 additions & 10 deletions src/models/player.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
use std::marker::PhantomData;

use base64::Engine;
use futures::{future, FutureExt, TryFutureExt};

use crate::constants::{
CREDITS_THRESHOLD_COEF, VIDEO_HASH_EXTRA_PROP, VIDEO_SIZE_EXTRA_PROP, WATCHED_THRESHOLD_COEF,
BASE64, CREDITS_THRESHOLD_COEF, PLAYER_IGNORE_SEEK_AFTER, VIDEO_HASH_EXTRA_PROP,
VIDEO_SIZE_EXTRA_PROP, WATCHED_THRESHOLD_COEF,
};
use crate::models::common::{
eq_update, resource_update, resources_update_with_vector_content, Loadable, ResourceAction,
ResourceLoadable, ResourcesAction,
eq_update, resource_update, resource_update_with_vector_content,
resources_update_with_vector_content, Loadable, ResourceAction, ResourceLoadable,
ResourcesAction,
};
use crate::models::ctx::Ctx;
use crate::models::ctx::{Ctx, CtxError};
use crate::runtime::msg::{Action, ActionLoad, ActionPlayer, Event, Internal, Msg};
use crate::runtime::{Effects, Env, UpdateWithCtx};
use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt, UpdateWithCtx};
use crate::types::addon::{AggrRequest, Descriptor, ExtraExt, ResourcePath, ResourceRequest};
use crate::types::api::{fetch_api, APIRequest, APIResult, SeekLogRequest, SuccessResponse};
use crate::types::library::{LibraryBucket, LibraryItem};
use crate::types::profile::Settings as ProfileSettings;
use crate::types::resource::{MetaItem, SeriesInfo, Stream, Subtitles, Video};
use crate::types::resource::{MetaItem, SeriesInfo, Stream, StreamSource, Subtitles, Video};

use stremio_watched_bitfield::WatchedBitField;

Expand All @@ -24,8 +30,6 @@ use serde::{Deserialize, Serialize};

use lazy_static::lazy_static;

use super::common::resource_update_with_vector_content;

lazy_static! {
/// The duration that must have passed in order for a library item to be updated.
pub static ref PUSH_TO_LIBRARY_EVERY: Duration = Duration::seconds(30);
Expand Down Expand Up @@ -57,6 +61,9 @@ pub struct AnalyticsContext {
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct VideoParams {
/// Opensubtitles hash usually retrieved from a streaming server endpoint.
///
/// It's used for requesting subtitles from Opensubtitles.
pub hash: Option<String>,
pub size: Option<u64>,
}
Expand Down Expand Up @@ -99,6 +106,17 @@ pub struct Player {
pub ended: bool,
#[serde(skip_serializing)]
pub paused: Option<bool>,
#[serde(skip_serializing)]
pub seek_history: Vec<SeekLog>,
}

#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SeekLog {
/// in milliseconds
pub from: u64,
/// in milliseconds
pub to: u64,
}

impl<E: Env + 'static> UpdateWithCtx<E> for Player {
Expand Down Expand Up @@ -260,6 +278,17 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
} else {
Effects::none().unchanged()
};
let seek_history_effects = seek_update::<E>(
self.selected.as_ref(),
self.video_params.as_ref(),
self.series_info.as_ref(),
self.library_item.as_ref(),
&mut self.seek_history,
// we do not have information whether the user is currently
// skipping the outro by Unloading the player.
None,
);

let switch_to_next_video_effects =
switch_to_next_video(&mut self.library_item, &self.next_video);
let push_to_library_effects = match &self.library_item {
Expand All @@ -284,7 +313,9 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
self.loaded = false;
self.ended = false;
self.paused = None;
switch_to_next_video_effects

seek_history_effects
.join(switch_to_next_video_effects)
.join(push_to_library_effects)
.join(selected_effects)
.join(video_params_effects)
Expand Down Expand Up @@ -326,7 +357,18 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
Some(library_item),
) => {
let seeking = library_item.state.time_offset.abs_diff(*time) > 1000;
// library_item.state.last_watched = Some(E::now() - chrono::Duration::days(1));

// 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,
});
}

library_item.state.last_watched = Some(E::now());
if library_item.state.video_id != Some(video_id.to_owned()) {
library_item.state.video_id = Some(video_id.to_owned());
Expand Down Expand Up @@ -451,6 +493,30 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
};
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(),
self.video_params.as_ref(),
self.series_info.as_ref(),
self.library_item.as_ref(),
&mut self.seek_history,
// use the current LibraryItem time offset as the outro time.
self.library_item
.as_ref()
.map(|library_item| library_item.state.time_offset),
);

// Load will actually take care of loading the next video

seek_history_effects.join(
Effects::msg(Msg::Event(Event::PlayerNextVideo {
context: self.analytics_context.as_ref().cloned().unwrap_or_default(),
is_binge_enabled: ctx.profile.settings.binge_watching,
is_playing_next_video: self.next_video.is_some(),
}))
.unchanged(),
)
}
Msg::Action(Action::Player(ActionPlayer::Ended)) if self.selected.is_some() => {
self.ended = true;
Effects::msg(Msg::Event(Event::PlayerEnded {
Expand Down Expand Up @@ -870,6 +936,84 @@ fn subtitles_update<E: Env + 'static>(
}
}

fn seek_update<E: Env + 'static>(
selected: Option<&Selected>,
video_params: Option<&VideoParams>,
series_info: Option<&SeriesInfo>,
library_item: Option<&LibraryItem>,
seek_history: &mut Vec<SeekLog>,
outro: Option<u64>,
) -> Effects {
// todo: Remove
tracing::info!(
"seek update starting... selected: {}; video_params: {}, series_info: {}, library_item: {}",
selected.is_some(),
video_params.is_some(),
series_info.is_some(),
library_item.is_some()
);

let seek_request_effects = match (selected, video_params, series_info, library_item) {
(Some(selected), Some(video_params), Some(series_info), Some(library_item)) => {
// todo: Remove
tracing::info!("seek update continues... is stream a torrent: {}; stream name: {}, video_params.hash: {}", matches!(selected.stream.source, StreamSource::Torrent { .. }), selected.stream.name.is_some(),
video_params.hash.is_some());

match (
&selected.stream.source,
selected.stream.name.as_ref(),
video_params.hash.clone(),
) {
(StreamSource::Torrent { .. }, Some(stream_name), Some(opensubtitles_hash)) => {
let filename_hash = {
use sha2::Digest;
let mut sha256 = sha2::Sha256::new();
sha256.update(stream_name);
let sha256_encoded = sha256.finalize();

BASE64.encode(sha256_encoded)
};

let seek_log_req = SeekLogRequest {
opensubtitles_hash,
item_id: library_item.id.to_owned(),
series_info: series_info.to_owned(),
filename_hash,
duration: library_item.state.duration,
seek_history: seek_history.to_owned(),
skip_outro: outro.map(|time| vec![time]).unwrap_or_default(),
};

// todo: Remove
tracing::info!("SeekLog API request: {seek_log_req:?}");

Effects::one(push_seek_to_api::<E>(seek_log_req)).unchanged()
}
_ => Effects::none().unchanged(),
}
}
_ => Effects::none().unchanged(),
};

seek_request_effects.join(eq_update(seek_history, vec![]))
}

fn push_seek_to_api<E: Env + 'static>(seek_log_req: SeekLogRequest) -> Effect {
let api_request = APIRequest::SeekLog(seek_log_req.clone());

EffectFuture::Concurrent(
fetch_api::<E, _, _, SuccessResponse>(&api_request)
.map_err(CtxError::from)
.and_then(|result| match result {
APIResult::Ok { result } => future::ok(result),
APIResult::Err { error } => future::err(CtxError::from(error)),
})
.map(move |result| Msg::Internal(Internal::SeekLogsResult(seek_log_req, result)))
.boxed_env(),
)
.into()
}

#[cfg(test)]
mod test {
use chrono::{TimeZone, Utc};
Expand Down
9 changes: 8 additions & 1 deletion src/runtime/msg/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
types::{
addon::Descriptor,
api::AuthRequest,
library::{LibraryItemId, LibraryItem},
library::LibraryItemId,
profile::Settings as ProfileSettings,
resource::{MetaItemId, MetaItemPreview, Video},
},
Expand Down Expand Up @@ -143,6 +143,13 @@ pub enum ActionPlayer {
PausedChanged {
paused: bool,
},
/// Play next video, if there is one, applicable to e.g.
/// movie series and playing the next episode.
NextVideo,
/// Video player has ended.
/// 2 scenarios are possible:
/// - We've watched a movie to the last second
/// - We've watched a movie series to the last episode and the last second
Ended,
}

Expand Down
5 changes: 5 additions & 0 deletions src/runtime/msg/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ pub enum Event {
PlayerStopped {
context: PlayerAnalyticsContext,
},
PlayerNextVideo {
context: PlayerAnalyticsContext,
is_binge_enabled: bool,
is_playing_next_video: bool,
},
PlayerEnded {
context: PlayerAnalyticsContext,
is_binge_enabled: bool,
Expand Down
6 changes: 5 additions & 1 deletion src/runtime/msg/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::runtime::EnvError;
use crate::types::addon::{Descriptor, Manifest, ResourceRequest, ResourceResponse};
use crate::types::api::{
APIRequest, AuthRequest, DataExportResponse, DatastoreRequest, LinkCodeResponse,
LinkDataResponse,
LinkDataResponse, SeekLogRequest, SuccessResponse,
};
use crate::types::library::{LibraryBucket, LibraryItem, LibraryItemId};
use crate::types::profile::{Auth, AuthKey, Profile, User};
Expand Down Expand Up @@ -104,6 +104,10 @@ pub enum Internal {
NotificationsRequestResult(ResourceRequest, Box<Result<ResourceResponse, EnvError>>),
/// Result for requesting a `dataExport` of user data.
DataExportResult(AuthKey, Result<DataExportResponse, CtxError>),
/// Result for submitting SeekLogs request for a played stream.
///
/// Applicable only to movie series and torrents.
SeekLogsResult(SeekLogRequest, Result<SuccessResponse, CtxError>),
/// The result of querying the data for LocalSearch
LoadLocalSearchResult(Url, Result<Vec<Searchable>, EnvError>),
}
25 changes: 25 additions & 0 deletions src/types/api/request.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::constants::{API_URL, LINK_API_URL};
use crate::models::player::SeekLog;
use crate::types::addon::Descriptor;
use crate::types::library::LibraryItem;
use crate::types::profile::{AuthKey, GDPRConsent, User};
use crate::types::resource::SeriesInfo;
#[cfg(test)]
use derivative::Derivative;
use http::Method;
Expand Down Expand Up @@ -53,6 +55,28 @@ pub enum APIRequest {
auth_key: AuthKey,
events: Vec<serde_json::Value>,
},
#[serde(rename_all = "camelCase")]
SeekLog(SeekLogRequest),
}

#[derive(Clone, PartialEq, Eq, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SeekLogRequest {
/// Opensubtitles hash returned by the server
#[serde(rename = "osId")]
pub opensubtitles_hash: String,
pub item_id: String,
#[serde(flatten)]
pub series_info: SeriesInfo,
/// Filename hash
///
/// base64 encoded SHA-256 hash of the Stream filename.
#[serde(rename = "stHash")]
pub filename_hash: String,
pub duration: u64,
pub seek_history: Vec<SeekLog>,
/// The time (in milliseconds) when the user decided to play the next video/episode
pub skip_outro: Vec<u64>,
}

impl FetchRequestParams<APIRequest> for APIRequest {
Expand All @@ -74,6 +98,7 @@ impl FetchRequestParams<APIRequest> for APIRequest {
APIRequest::SaveUser { .. } => "saveUser".to_owned(),
APIRequest::DataExport { .. } => "dataExport".to_owned(),
APIRequest::Events { .. } => "events".to_owned(),
APIRequest::SeekLog { .. } => "seekLog".to_owned(),
}
}
fn query(&self) -> Option<String> {
Expand Down

0 comments on commit 61302b1

Please sign in to comment.