Skip to main content

Overview

The LibraryWithFilters model provides a filtered, sorted, and paginated view of the user’s library. It supports different filter types and sorting strategies for organizing media collections.

Structure

pub struct LibraryWithFilters<F> {
    pub selected: Option<Selected>,
    pub selectable: Selectable,
    pub catalog: Vec<LibraryItem>,
    pub filter: PhantomData<F>,
}

LibraryItem

Represents a single item in the user’s library:
pub struct LibraryItem {
    pub id: LibraryItemId,
    pub name: String,
    pub r#type: String,
    pub poster: Option<Url>,
    pub poster_shape: PosterShape,
    pub removed: bool,
    pub temp: bool,
    pub ctime: Option<DateTime<Utc>>,
    pub mtime: DateTime<Utc>,
    pub state: LibraryItemState,
    pub behavior_hints: MetaItemBehaviorHints,
}

LibraryItemState

pub struct LibraryItemState {
    pub last_watched: Option<DateTime<Utc>>,
    pub time_offset: u64,
    pub duration: u64,
    pub video_id: Option<String>,
    pub watched: Option<WatchedField>,
    pub last_vid_released: Option<DateTime<Utc>>,
    pub no_notif: bool,
    pub flagged_watched: u32,
    pub times_watched: u32,
    pub time_watched: u64,
    pub overall_time_watched: u64,
}

Filter Types

ContinueWatchingFilter

Shows items currently being watched:
impl LibraryFilter for ContinueWatchingFilter {
    fn predicate(library_item: &LibraryItem, notifications: &NotificationsBucket) -> bool {
        let has_notification = notifications
            .items
            .get(&library_item.id)
            .filter(|notifs| !notifs.is_empty())
            .is_some();
        
        library_item.is_in_continue_watching() || has_notification
    }
}
Include items that:
  • Have time_offset > 0 (started watching)
  • Are not removed or are temporary
  • Type is not “other”
  • Have pending notifications

NotRemovedFilter

Shows all active library items:
impl LibraryFilter for NotRemovedFilter {
    fn predicate(library_item: &LibraryItem, _: &NotificationsBucket) -> bool {
        !library_item.removed
    }
}

Sorting Options

pub enum Sort {
    LastWatched,      // Most recently watched first
    Name,             // Alphabetical A-Z
    NameReverse,      // Alphabetical Z-A
    TimesWatched,     // Most watched first
    Watched,          // Watched items first, then by last_watched
    NotWatched,       // Unwatched items first
}

Sort Implementation

impl Sort {
    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),
            
            Sort::Watched => 
                b.watched()
                    .cmp(&a.watched())
                    .then(b.state.last_watched.cmp(&a.state.last_watched))
                    .then(b.ctime.cmp(&a.ctime)),
            
            Sort::NotWatched => 
                a.watched()
                    .cmp(&b.watched())
                    .then(a.state.last_watched.cmp(&b.state.last_watched))
                    .then(a.ctime.cmp(&b.ctime)),
            
            Sort::Name => 
                a.name.to_lowercase().cmp(&b.name.to_lowercase()),
            
            Sort::NameReverse => 
                b.name.to_lowercase().cmp(&a.name.to_lowercase()),
        }
    }
}

Request & Response Models

LibraryRequest

pub struct LibraryRequest {
    pub r#type: Option<String>,  // "movie", "series", etc.
    pub sort: Sort,
    pub page: LibraryRequestPage,
}

Selectable Types

Provide UI-ready filter and sort options:
pub struct SelectableType {
    pub r#type: Option<String>,
    pub selected: bool,
    pub request: LibraryRequest,
}

pub struct SelectableSort {
    pub sort: Sort,
    pub selected: bool,
    pub request: LibraryRequest,
}

pub struct SelectablePage {
    pub request: LibraryRequest,  // For next page
}

pub struct Selectable {
    pub types: Vec<SelectableType>,
    pub sorts: Vec<SelectableSort>,
    pub next_page: Option<SelectablePage>,
}

Actions

Load Library

Msg::Action(Action::Load(ActionLoad::LibraryWithFilters(selected)))
1

Update Selection

Sets the current filter, sort, and page
2

Update Selectables

Rebuilds available types and sorts based on current library
3

Update Catalog

Filters, sorts, and paginates items

Load Next Page

Msg::Action(Action::LibraryWithFilters(ActionLibraryWithFilters::LoadNextPage))
Increments page number and appends more items to the catalog.

Pagination

Pages are 1-indexed and use CATALOG_PAGE_SIZE (typically 100 items):
pub struct LibraryRequestPage(pub NonZeroUsize);

impl Default for LibraryRequestPage {
    fn default() -> Self {
        LibraryRequestPage(NonZeroUsize::new(1).unwrap())
    }
}
Next Page Calculation:
let next_page = library
    .items
    .values()
    .filter(|item| F::predicate(item, notifications))
    .filter(|item| matches_type_filter(item, &selected.request.r#type))
    .nth(selected.request.page.get() * CATALOG_PAGE_SIZE)
    .map(|_| SelectablePage {
        request: LibraryRequest {
            page: LibraryRequestPage(
                NonZeroUsize::new(selected.request.page.get() + 1).unwrap()
            ),
            ..selected.request.to_owned()
        },
    });

LibraryItem Methods

Watching Status

impl LibraryItem {
    /// Check if item should appear in Continue Watching
    pub fn is_in_continue_watching(&self) -> bool {
        self.r#type != "other" 
            && (!self.removed || self.temp) 
            && self.state.time_offset > 0
    }
    
    /// Get watch progress percentage
    pub fn progress(&self) -> f64 {
        if self.state.time_offset > 0 && self.state.duration > 0 {
            (self.state.time_offset as f64 / self.state.duration as f64) * 100.0
        } else {
            0.0
        }
    }
    
    /// Check if watched at least once
    pub fn watched(&self) -> bool {
        self.state.times_watched > 0
    }
}

Marking as Watched

pub fn mark_as_watched<E: Env>(&mut self, is_watched: bool) {
    if is_watched {
        self.state.times_watched = self.state.times_watched.saturating_add(1);
        self.state.last_watched = Some(E::now());
    } else {
        self.state.times_watched = 0;
    }
}

pub fn mark_video_as_watched<E: Env>(
    &mut self,
    watched: &WatchedBitField,
    video: &Video,
    is_watched: bool,
) {
    let mut watched = watched.to_owned();
    watched.set_video(&video.id, is_watched);
    self.state.watched = Some(watched.into());
    // Updates last_watched if newer than current
}

Notifications

pub fn should_pull_notifications(&self) -> bool {
    !self.state.no_notif
        && self.r#type != "other"
        && self.r#type != "movie"
        && self.behavior_hints.default_video_id.is_none()
        && !self.removed
        && !self.temp
}
Notifications are pulled only for series (not movies) that are actively in the user’s library and don’t have a default video set.

Usage Example

use stremio_core::models::library_with_filters::{
    LibraryWithFilters, NotRemovedFilter, Sort, LibraryRequest
};

// Initialize library model
let (library_model, effects) = LibraryWithFilters::<NotRemovedFilter>::new(
    &ctx.library,
    &ctx.notifications
);

// Load first page of movies sorted by last watched
let request = LibraryRequest {
    r#type: Some("movie".to_string()),
    sort: Sort::LastWatched,
    page: LibraryRequestPage::default(),
};

runtime.dispatch(Msg::Action(
    Action::Load(ActionLoad::LibraryWithFilters(Selected { request }))
));

// Access filtered catalog
for item in &library_model.catalog {
    println!("{}: {}% watched", item.name, item.progress());
}

Best Practices

Types are sorted by priority: movie, series, channel, tv, then others. This ensures consistent ordering in the UI.
The model only recalculates selectables and catalog when the underlying library changes or when explicitly requested.
Items with temp: true and removed: true are automatically cleaned up after being marked as watched.

Build docs developers (and LLMs) love