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: Addon watch status resource #517

Draft
wants to merge 8 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ members = [
"stremio-watched-bitfield",
]

[lib]
doctest = false
# [lib]
# doctest = false

[features]
# TODO: env-future-send should be enabled by default
Expand Down
56 changes: 56 additions & 0 deletions examples/resource_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use stremio_core::{
constants::{
CATALOG_RESOURCE_NAME, CINEMETA_TOP_CATALOG_ID, CINEMETA_URL, STREAM_RESOURCE_NAME,
WATCH_STATUS_RESOURCE_NAME,
},
types::{
addon::{ExtraValue, ResourcePath, ResourceRequest},
watch_status,
},
};

fn main() {
let _cinemeta_resource_request = ResourceRequest {
base: CINEMETA_URL.to_owned(),
path: ResourcePath {
id: CINEMETA_TOP_CATALOG_ID.to_owned(),
resource: CATALOG_RESOURCE_NAME.to_owned(),
r#type: "movie".to_owned(),
extra: vec![ExtraValue {
name: "genre".to_owned(),
value: "your-genre".to_owned(),
}],
},
};

let watch_status_request = watch_status::Request::Resume {
// 1 hour mark in milliseconds
current_time: (60_u64 * 60 * 1000),
// 1.5 hours in milliseconds
duration: (90_u64 * 60 * 1000),
};

let watch_status_resource_request = ResourceRequest {
base: CINEMETA_URL.to_owned(),
path: ResourcePath {
id: "tt0944947".to_owned(),
resource: WATCH_STATUS_RESOURCE_NAME.to_owned(),
r#type: "series".to_owned(),
extra: watch_status_request.into(),
},
};

let watch_status_path = watch_status_resource_request.path.to_url_path();
println!("watchStatus 'play' extraArgs: {}", watch_status_path);
assert_eq!(
"/watchStatus/series/tt0944947/action=resume&currentTime=3600000&duration=5400000.json",
watch_status_path
);

let _stream_path = ResourcePath {
resource: STREAM_RESOURCE_NAME.to_owned(),
r#type: "serial".to_owned(),
id: "tt0944947".to_owned(),
extra: vec![],
};
}
22 changes: 3 additions & 19 deletions src/addon_transport/http_transport/http_transport.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use crate::addon_transport::http_transport::legacy::AddonLegacyTransport;
use crate::addon_transport::AddonTransport;
use crate::constants::{ADDON_LEGACY_PATH, ADDON_MANIFEST_PATH, URI_COMPONENT_ENCODE_SET};
use crate::constants::{ADDON_LEGACY_PATH, ADDON_MANIFEST_PATH};
use crate::runtime::{Env, EnvError, EnvFutureExt, TryEnvFuture};
use crate::types::addon::{Manifest, ResourcePath, ResourceResponse};
use crate::types::query_params_encode;
use futures::future;
use http::Request;
use percent_encoding::utf8_percent_encode;
use std::marker::PhantomData;
use url::Url;

Expand Down Expand Up @@ -40,27 +38,13 @@ impl<E: Env> AddonTransport for AddonHTTPTransport<E> {
)))
.boxed_env();
}
let path = if path.extra.is_empty() {
format!(
"/{}/{}/{}.json",
utf8_percent_encode(&path.resource, URI_COMPONENT_ENCODE_SET),
utf8_percent_encode(&path.r#type, URI_COMPONENT_ENCODE_SET),
utf8_percent_encode(&path.id, URI_COMPONENT_ENCODE_SET),
)
} else {
format!(
"/{}/{}/{}/{}.json",
utf8_percent_encode(&path.resource, URI_COMPONENT_ENCODE_SET),
utf8_percent_encode(&path.r#type, URI_COMPONENT_ENCODE_SET),
utf8_percent_encode(&path.id, URI_COMPONENT_ENCODE_SET),
query_params_encode(path.extra.iter().map(|ev| (&ev.name, &ev.value)))
)
};
let path = path.to_url_path();
let url = self
.transport_url
.as_str()
.replace(ADDON_MANIFEST_PATH, &path);
let request = Request::get(&url).body(()).expect("request builder failed");

E::fetch(request)
}
fn manifest(&self) -> TryEnvFuture<Manifest> {
Expand Down
23 changes: 0 additions & 23 deletions src/addon_transport/http_transport/legacy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,29 +65,6 @@ pub struct SubtitlesResult {
pub all: Vec<Subtitles>,
}

impl From<Vec<MetaItemPreview>> for ResourceResponse {
fn from(metas: Vec<MetaItemPreview>) -> Self {
ResourceResponse::Metas { metas }
}
}
impl From<MetaItem> for ResourceResponse {
fn from(meta: MetaItem) -> Self {
ResourceResponse::Meta { meta }
}
}
impl From<Vec<Stream>> for ResourceResponse {
fn from(streams: Vec<Stream>) -> Self {
ResourceResponse::Streams { streams }
}
}
impl From<SubtitlesResult> for ResourceResponse {
fn from(subtitles_result: SubtitlesResult) -> Self {
ResourceResponse::Subtitles {
subtitles: subtitles_result.all,
}
}
}

fn map_response<T: Sized + ConditionalSend + 'static>(resp: JsonRPCResp<T>) -> TryEnvFuture<T> {
match resp {
JsonRPCResp::Result { result } => future::ok(result).boxed_env(),
Expand Down
2 changes: 1 addition & 1 deletion src/addon_transport/http_transport/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod legacy;
pub(crate) mod legacy;

mod http_transport;
pub use http_transport::*;
3 changes: 3 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub const META_RESOURCE_NAME: &str = "meta";
pub const STREAM_RESOURCE_NAME: &str = "stream";
/// `https://{ADDON_URL}/catalog/...` resource
pub const CATALOG_RESOURCE_NAME: &str = "catalog";
/// `https://{ADDON_URL}/watchStatus/...` resource
pub const WATCH_STATUS_RESOURCE_NAME: &str = "watchStatus";
/// `https://{ADDON_URL}/subtitles/...` resource
pub const SUBTITLES_RESOURCE_NAME: &str = "subtitles";
pub const ADDON_MANIFEST_PATH: &str = "/manifest.json";
pub const ADDON_LEGACY_PATH: &str = "/stremio/v1";
Expand Down
142 changes: 135 additions & 7 deletions src/models/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::marker::PhantomData;

use crate::constants::{
CREDITS_THRESHOLD_COEF, VIDEO_HASH_EXTRA_PROP, VIDEO_SIZE_EXTRA_PROP, WATCHED_THRESHOLD_COEF,
WATCH_STATUS_RESOURCE_NAME,
};
use crate::models::common::{
eq_update, resource_update, resources_update_with_vector_content, Loadable, ResourceAction,
Expand All @@ -10,21 +11,22 @@ use crate::models::common::{
use crate::models::ctx::Ctx;
use crate::runtime::msg::{Action, ActionLoad, ActionPlayer, Event, Internal, Msg};
use crate::runtime::{Effects, Env, UpdateWithCtx};
use crate::types::addon::{AggrRequest, ExtraExt, ResourcePath, ResourceRequest};
use crate::types::addon::{AggrRequest, Descriptor, ExtraExt, ResourcePath, ResourceRequest};
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::watch_status;

use stremio_watched_bitfield::WatchedBitField;

use chrono::{DateTime, Duration, Utc};
use chrono::{DateTime, Duration, TimeZone, Utc};
use derivative::Derivative;
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use lazy_static::lazy_static;

use super::common::resource_update_with_vector_content;
use super::common::{resource_update_with_vector_content, resources_update};

lazy_static! {
/// The duration that must have passed in order for a library item to be updated.
Expand Down Expand Up @@ -72,7 +74,8 @@ pub struct Selected {
pub video_params: Option<VideoParams>,
}

#[derive(Default, Clone, Derivative, Serialize, Debug)]
#[derive(Clone, Derivative, Serialize, Debug)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct Player {
pub selected: Option<Selected>,
Expand All @@ -97,6 +100,8 @@ pub struct Player {
pub ended: bool,
#[serde(skip_serializing)]
pub paused: Option<bool>,
#[serde(skip)]
pub watch_status: Vec<ResourceLoadable<watch_status::Response>>,
}

impl<E: Env + 'static> UpdateWithCtx<E> for Player {
Expand Down Expand Up @@ -225,6 +230,13 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
let watched_effects =
watched_update(&mut self.watched, &self.meta_item, &self.library_item);

let watched_status_effects = watch_status_update::<E>(
msg,
&ctx.profile.addons,
self.library_item.as_ref(),
&mut self.watch_status,
);

// dismiss LibraryItem notification if we have a LibraryItem to begin with
let notification_effects = match &self.library_item {
Some(library_item) => Effects::msg(Msg::Internal(
Expand Down Expand Up @@ -271,6 +283,8 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
.join(series_info_effects)
.join(library_item_effects)
.join(watched_effects)
// after LibraryItem in order to have the LibraryItem updated and `Some`
.join(watched_status_effects)
.join(notification_effects)
}
Msg::Action(Action::Unload) => {
Expand Down Expand Up @@ -299,6 +313,14 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
let series_info_effects = eq_update(&mut self.series_info, None);
let library_item_effects = eq_update(&mut self.library_item, None);
let watched_effects = eq_update(&mut self.watched, None);
// todo: is this the best place to update watch status?
let watched_status_effects = watch_status_update::<E>(
msg,
&ctx.profile.addons,
self.library_item.as_ref(),
&mut self.watch_status,
);

self.analytics_context = None;
self.load_time = None;
self.loaded = false;
Expand All @@ -315,6 +337,7 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
.join(library_item_effects)
.join(watched_effects)
.join(ended_effects)
.join(watched_status_effects)
}
Msg::Action(Action::Player(ActionPlayer::TimeChanged {
time,
Expand Down Expand Up @@ -456,16 +479,35 @@ impl<E: Env + 'static> UpdateWithCtx<E> for Player {
.unchanged(),
_ => Effects::none().unchanged(),
};
trakt_event_effects.join(update_library_item_effects)

let watched_status_effects = watch_status_update::<E>(
msg,
&ctx.profile.addons,
self.library_item.as_ref(),
&mut self.watch_status,
);
trakt_event_effects
.join(update_library_item_effects)
.join(watched_status_effects)
}
Msg::Action(Action::Player(ActionPlayer::Ended)) if self.selected.is_some() => {
self.ended = true;
Effects::msg(Msg::Event(Event::PlayerEnded {

let ended_effects = Effects::msg(Msg::Event(Event::PlayerEnded {
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()
.unchanged();

let watched_status_effects = watch_status_update::<E>(
msg,
&ctx.profile.addons,
self.library_item.as_ref(),
&mut self.watch_status,
);

ended_effects.join(watched_status_effects)
}
Msg::Internal(Internal::ResourceRequestResult(request, result)) => {
let meta_item_effects = match &mut self.meta_item {
Expand Down Expand Up @@ -802,6 +844,92 @@ fn watched_update(
.map(|(meta_item, library_item)| library_item.state.watched_bitfield(&meta_item.videos));
eq_update(watched, next_watched)
}

fn watch_status_update<E: Env + 'static>(
msg: &Msg,
addons: &[Descriptor],
library_item: Option<&LibraryItem>,
watch_status: &mut Vec<ResourceLoadable<watch_status::Response>>,
) -> Effects {
match library_item {
Some(library_item) => {
let watch_status_request = match msg {
Msg::Action(Action::Load(ActionLoad::Player(_selected))) => {
// TODO: Double check if this is current time!
let current_time = library_item.state.time_offset;
// TODO: Double check if this is duration!
let duration = library_item.state.duration;
watch_status::Request::Start {
current_time,
duration,
}
}
Msg::Action(Action::Player(ActionPlayer::PausedChanged { paused })) => {
// TODO: Double check if this is current time!
let current_time = library_item.state.time_offset;
// TODO: Double check if this is duration!
let duration = library_item.state.duration;

if *paused {
watch_status::Request::Pause {
current_time,
duration,
}
} else {
watch_status::Request::Resume {
current_time,
duration,
}
}
}
Msg::Action(Action::Player(ActionPlayer::Ended)) => {
// TODO: Double check if this is current time!
let current_time = library_item.state.time_offset;
// TODO: Double check if this is duration!
let duration = library_item.state.duration;

watch_status::Request::End {
current_time,
duration,
}
}
Msg::Action(Action::Unload) => {
// TODO: Double check if this is current time!
let current_time = library_item.state.time_offset;
// TODO: Double check if this is duration!
let duration = library_item.state.duration;

watch_status::Request::End {
current_time,
duration,
}
}
_ => return Effects::none().unchanged(),
};

let watch_status_resource_effects = resources_update::<E, _>(
watch_status,
// force the making of a requests every time we have WatchStatus changes
ResourcesAction::force_request(
&AggrRequest::AllOfResource(ResourcePath {
id: library_item.id.to_owned(),
resource: WATCH_STATUS_RESOURCE_NAME.to_string(),
extra: watch_status_request.into(),
r#type: library_item.r#type.to_owned(),
}),
addons,
),
);

Effects::none()
.unchanged()
.join(watch_status_resource_effects)
}

None => Effects::none().unchanged(),
}
}

#[cfg(test)]
mod test {
use chrono::{TimeZone, Utc};
Expand Down
Loading