Skip to content

Commit

Permalink
Merge pull request #647 from Stremio/feat/sort-not-watched-and-watched
Browse files Browse the repository at this point in the history
Feat/sort not watched and watched
  • Loading branch information
elpiel authored Mar 8, 2024
2 parents a90bb16 + 356f9a1 commit 800ec84
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 19 deletions.
14 changes: 3 additions & 11 deletions src/models/library_by_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,7 @@ fn catalogs_update<F: LibraryFilter>(
.unwrap_or(CATALOG_PAGE_SIZE);
library_items
.into_iter()
.sorted_by(|a, b| match &selected.sort {
Sort::LastWatched => b.state.last_watched.cmp(&a.state.last_watched),
Sort::TimesWatched => b.state.times_watched.cmp(&a.state.times_watched),
Sort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
})
.sorted_by(|a, b| selected.sort.sort_items(a, b))
.take(take)
.collect::<Vec<_>>()
.chunks(CATALOG_PAGE_SIZE)
Expand All @@ -212,12 +208,8 @@ fn next_page<F: LibraryFilter>(
.items
.values()
.filter(|library_item| F::predicate(library_item, notifications))
.filter(|library_item| library_item.r#type == *r#type)
.sorted_by(|a, b| match &selected.sort {
Sort::LastWatched => b.state.last_watched.cmp(&a.state.last_watched),
Sort::TimesWatched => b.state.times_watched.cmp(&a.state.times_watched),
Sort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
})
.filter(|library_item: &&LibraryItem| library_item.r#type == *r#type)
.sorted_by(|a, b| selected.sort.sort_items(a, b))
.skip(skip)
.take(CATALOG_PAGE_SIZE)
.map(|library_item| (*library_item).to_owned())
Expand Down
201 changes: 195 additions & 6 deletions src/models/library_with_filters.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{iter, marker::PhantomData, num::NonZeroUsize};
use std::{cmp::Ordering, iter, marker::PhantomData, num::NonZeroUsize};

use derivative::Derivative;
use derive_more::Deref;
Expand Down Expand Up @@ -57,6 +57,37 @@ pub enum Sort {
LastWatched,
Name,
TimesWatched,
Watched,
NotWatched,
}

impl Sort {
/// [`Sort`]ing the two given [`LibraryItem`]s for the Library
pub fn sort_items(&self, a: &LibraryItem, b: &LibraryItem) -> Ordering {
match &self {
Sort::LastWatched => b.state.last_watched.cmp(&a.state.last_watched),
Sort::TimesWatched => b.state.times_watched.cmp(&a.state.times_watched),
// the only difference between the Watched and Not watched sorting
// is the ordering of the `a` and `b` items
Sort::Watched => b
.watched()
.cmp(&a.watched())
.then(b.state.last_watched.cmp(&a.state.last_watched))
// only as fallback
// when a new item is added to the library, `last_watched` is always set to now
// same as `ctime`
.then(b.ctime.cmp(&a.ctime)),
Sort::NotWatched => a
.watched()
.cmp(&b.watched())
.then(a.state.last_watched.cmp(&b.state.last_watched))
// only as fallback
// when a new item is added to the library, `last_watched` is always set to now
// same as `ctime`
.then(a.ctime.cmp(&b.ctime)),
Sort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
}
}
}

#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -326,15 +357,173 @@ fn catalog_update<F: LibraryFilter>(
Some(r#type) => library_item.r#type == *r#type,
None => true,
})
.sorted_by(|a, b| match &selected.request.sort {
Sort::LastWatched => b.state.last_watched.cmp(&a.state.last_watched),
Sort::TimesWatched => b.state.times_watched.cmp(&a.state.times_watched),
Sort::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
})
.sorted_by(|a, b| selected.request.sort.sort_items(a, b))
.take(selected.request.page.get() * CATALOG_PAGE_SIZE)
.cloned()
.collect(),
_ => vec![],
};
eq_update(catalog, next_catalog)
}

#[cfg(test)]
mod test {
use chrono::{Duration, Utc};

use crate::types::{
library::{LibraryItem, LibraryItemState},
resource::PosterShape,
};

use super::Sort;

#[test]
fn test_watched_and_not_watched_sort_items_ordering_of_library_items() {
// For series, times_watched is incremented to indicate that a single or more
// episodes have been watched
// While last_watched is used to order the watched items
// And flagged_watched is not used to show the watched indicator
let watched_latest_series = LibraryItem {
id: "tt13622776".into(),
name: "Ahsoka".into(),
r#type: "series".into(),
poster: None,
poster_shape: PosterShape::Poster,
removed: false,
temp: false,
ctime: Some(Utc::now()),
mtime: Utc::now(),
state: LibraryItemState {
last_watched: Some(Utc::now()),
// Series has been watched
flagged_watched: 1,
// indicate 2 watched videos
times_watched: 2,
..Default::default()
},
behavior_hints: crate::types::resource::MetaItemBehaviorHints::default(),
};
let watched_movie_1_week_ago = LibraryItem {
id: "tt15398776".into(),
name: "Oppenheimer".into(),
r#type: "movie".into(),
poster: None,
poster_shape: PosterShape::Poster,
removed: false,
temp: false,
ctime: Some(Utc::now()),
mtime: Utc::now(),
state: LibraryItemState {
last_watched: Some(Utc::now() - Duration::weeks(1)),
flagged_watched: 1,
..Default::default()
},
behavior_hints: crate::types::resource::MetaItemBehaviorHints::default(),
};

let not_watched_movie_added_3_weeks_ago = LibraryItem {
id: "tt2267998".into(),
name: "Gone Girl".into(),
r#type: "movie".into(),
poster: None,
poster_shape: PosterShape::Poster,
removed: false,
temp: false,
ctime: Some(Utc::now() - Duration::weeks(3)),
mtime: Utc::now(),
state: LibraryItemState {
last_watched: Some(Utc::now() - Duration::weeks(3)),
flagged_watched: 0,
times_watched: 0,
..Default::default()
},
behavior_hints: crate::types::resource::MetaItemBehaviorHints::default(),
};

let not_watched_movie_added_2_weeks_ago = LibraryItem {
id: "tt0118715".into(),
name: "The Big Lebowski".into(),
r#type: "movie".into(),
poster: None,
poster_shape: PosterShape::Poster,
removed: false,
temp: false,
ctime: Some(Utc::now() - Duration::weeks(2)),
mtime: Utc::now(),
state: LibraryItemState {
last_watched: Some(Utc::now() - Duration::weeks(2)),
flagged_watched: 0,
times_watched: 0,
..Default::default()
},
behavior_hints: crate::types::resource::MetaItemBehaviorHints::default(),
};

let watched_movie_1_week_ago_marked_not_watched = LibraryItem {
id: "tt1462764".into(),
name: "Indiana Jones and the Dial of Destiny".into(),
r#type: "movie".into(),
poster: None,
poster_shape: PosterShape::Poster,
removed: false,
temp: false,
ctime: Some(Utc::now() - Duration::weeks(4)),
mtime: Utc::now(),
state: LibraryItemState {
last_watched: Some(Utc::now() - Duration::weeks(1)),
flagged_watched: 0,
times_watched: 0,
..Default::default()
},
behavior_hints: crate::types::resource::MetaItemBehaviorHints::default(),
};

// Sort by Watched - first library items that are Watched by latest `last_watched` desc
// and then not watched and creation time (`ctime`) desc
{
let mut items = vec![
&not_watched_movie_added_3_weeks_ago,
&watched_movie_1_week_ago_marked_not_watched,
&not_watched_movie_added_2_weeks_ago,
&watched_movie_1_week_ago,
&watched_latest_series,
];

items.sort_by(|a, b| Sort::Watched.sort_items(a, b));

pretty_assertions::assert_eq!(
items,
vec![
&watched_latest_series,
&watched_movie_1_week_ago,
&watched_movie_1_week_ago_marked_not_watched,
&not_watched_movie_added_2_weeks_ago,
&not_watched_movie_added_3_weeks_ago,
]
)
}

{
let mut items = vec![
&not_watched_movie_added_3_weeks_ago,
&watched_latest_series,
&not_watched_movie_added_2_weeks_ago,
&watched_movie_1_week_ago,
&watched_movie_1_week_ago_marked_not_watched,
];

items.sort_by(|a, b| Sort::NotWatched.sort_items(a, b));

pretty_assertions::assert_eq!(
items,
vec![
&not_watched_movie_added_3_weeks_ago,
&not_watched_movie_added_2_weeks_ago,
&watched_movie_1_week_ago_marked_not_watched,
&watched_movie_1_week_ago,
&watched_latest_series,
]
)
}
}
}
5 changes: 3 additions & 2 deletions src/types/library/library_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@ pub struct LibraryItemState {
pub overall_time_watched: u64,
/// Shows how many times this item has been watched.
///
/// Incremented once for each video watched
/// or in the case of no videos - every time
/// Incremented once for each video watched (series)
/// or in the case of no videos (e.g. movies) - every time
pub times_watched: u32,
// @TODO: consider bool that can be deserialized from an integer
/// Flag indicating that a movie has been watched
pub flagged_watched: u32,
/// In milliseconds
pub duration: u64,
Expand Down

0 comments on commit 800ec84

Please sign in to comment.