Skip to main content

Overview

The MetaDetails model aggregates metadata from multiple addons, manages stream selection, tracks library state, and provides watch progress and rating functionality.

Structure

pub struct MetaDetails {
    pub selected: Option<Selected>,
    pub meta_items: Vec<ResourceLoadable<MetaItem>>,
    pub meta_streams: Vec<ResourceLoadable<Vec<Stream>>>,
    pub streams: Vec<ResourceLoadable<Vec<Stream>>>,
    pub last_used_stream: Option<ResourceLoadable<Option<Stream>>>,
    pub library_item: Option<LibraryItem>,
    pub rating_info: Option<Loadable<RatingInfo, EnvError>>,
    pub watched: Option<WatchedBitField>,
}

Selected

Defines what to load:
pub struct Selected {
    pub meta_path: ResourcePath,
    pub stream_path: Option<ResourcePath>,
    pub guess_stream: bool,
}
Fields:
  • meta_path - Resource path for metadata (e.g., catalog/movie/tt1254207)
  • stream_path - Optional path for streams (e.g., stream/movie/tt1254207)
  • guess_stream - Auto-select stream based on metadata

Stream Guessing

When guess_stream: true and stream_path: None:
1

Wait for Meta

Waits for first successful MetaItem load
2

Find Video ID

Uses behavior_hints.default_video_id if available, otherwise uses meta.id if no videos
3

Override Selection

Updates stream_path and sets guess_stream: false
fn selected_guess_stream_update(
    selected: &mut Option<Selected>,
    meta_items: &[ResourceLoadable<MetaItem>],
) -> Effects {
    // Wait for all requests to complete
    let meta_item = if meta_items.iter().all(|item| {
        matches!(item.content, Some(Loadable::Ready(..)))
            || matches!(item.content, Some(Loadable::Err(..)))
    }) {
        meta_items.iter().find_map(|item| match &item.content {
            Some(Loadable::Ready(meta)) => Some(meta),
            _ => None,
        })
    } else {
        return Effects::default();
    };
    
    let video_id = match (
        meta_item.videos.len(),
        &meta_item.preview.behavior_hints.default_video_id,
    ) {
        (_, Some(default_video_id)) => default_video_id.to_owned(),
        (0, None) => meta_item.preview.id.to_owned(),
        _ => return Effects::default(),
    };
    
    eq_update(selected, Some(Selected {
        stream_path: Some(ResourcePath {
            resource: "stream".to_owned(),
            r#type: meta_path.r#type.to_owned(),
            id: video_id,
            extra: vec![],
        }),
        guess_stream: false,
        // ...
    }))
}

Meta Items

Requests metadata from all addons supporting the resource:
fn meta_items_update<E: Env + 'static>(
    meta_items: &mut Vec<ResourceLoadable<MetaItem>>,
    selected: &Option<Selected>,
    profile: &Profile,
) -> Effects {
    match selected {
        Some(Selected { meta_path, .. }) => resources_update::<E, _>(
            meta_items,
            ResourcesAction::ResourcesRequested {
                request: &AggrRequest::AllOfResource(meta_path.to_owned()),
                addons: &profile.addons,
                force: false,  // Use cached if available
            },
        ),
        _ => eq_update(meta_items, vec![]),
    }
}

Streams

Meta Streams

Streams embedded in the MetaItem itself:
fn meta_streams_update(
    meta_streams: &mut Vec<ResourceLoadable<Vec<Stream>>>,
    selected: &Option<Selected>,
    meta_items: &[ResourceLoadable<MetaItem>],
) -> Effects {
    match selected {
        Some(Selected { stream_path: Some(stream_path), .. }) => {
            let streams = meta_items
                .iter()
                .find_map(|meta_item| match &meta_item.content {
                    Some(Loadable::Ready(meta)) => Some((meta_item.request, meta)),
                    _ => None,
                })
                .and_then(|(request, meta)| {
                    meta.videos
                        .iter()
                        .find(|v| v.id == stream_path.id)
                        .and_then(|video| {
                            if !video.streams.is_empty() {
                                Some(Cow::Borrowed(&video.streams))
                            } else {
                                // Fallback to YouTube stream
                                Stream::youtube(&video.id)
                                    .map(|s| vec![s])
                                    .map(Cow::Owned)
                            }
                        })
                        .map(|streams| (request, streams))
                })
                .map(|(request, streams)| ResourceLoadable {
                    request: ResourceRequest {
                        base: request.base.to_owned(),
                        path: ResourcePath {
                            resource: "stream".to_owned(),
                            r#type: request.path.r#type.to_owned(),
                            id: stream_path.id.to_owned(),
                            extra: request.path.extra.to_owned(),
                        },
                    },
                    content: Some(Loadable::Ready(streams.into_owned())),
                })
                .into_iter()
                .collect();
            
            eq_update(meta_streams, streams)
        }
        _ => Effects::none().unchanged(),
    }
}

Addon Streams

Streams from dedicated stream addons:
fn streams_update<E: Env + 'static>(
    streams: &mut Vec<ResourceLoadable<Vec<Stream>>>,
    selected: &Option<Selected>,
    profile: &Profile,
) -> Effects {
    match selected {
        Some(Selected { stream_path: Some(stream_path), .. }) => {
            resources_update_with_vector_content::<E, _>(
                streams,
                ResourcesAction::ResourcesRequested {
                    request: &AggrRequest::AllOfResource(stream_path.to_owned()),
                    addons: &profile.addons,
                    force: false,
                },
            )
        }
        _ => eq_update(streams, vec![]),
    }
}

Last Used Stream

Finds the most appropriate stream for binge watching:
1

Find Latest Stream Item

Searches last 30 videos for a stored StreamItem
2

Match Transport URL

Finds addon responses from the same transport URL
3

Find Stream

Matches by source equality, then by binge group
fn last_used_stream_update(
    last_used_stream: &mut Option<ResourceLoadable<Option<Stream>>>,
    selected: &Option<Selected>,
    meta_items: &[ResourceLoadable<MetaItem>],
    meta_streams: &[ResourceLoadable<Vec<Stream>>],
    streams: &[ResourceLoadable<Vec<Stream>>],
    stream_bucket: &StreamsBucket,
) -> Effects {
    let all_streams = [meta_streams, streams].concat();
    
    let next_stream = match selected {
        Some(Selected { stream_path: Some(stream_path), .. }) => {
            meta_items
                .iter()
                .filter(|_| !all_streams.is_empty())
                .find_map(|meta_item| match &meta_item.content {
                    Some(Loadable::Ready(meta)) => {
                        stream_bucket
                            .last_stream_item(&stream_path.id, meta)
                            .and_then(|stream_item| {
                                all_streams
                                    .iter()
                                    .find(|res| {
                                        res.request.base == stream_item.stream_transport_url
                                    })
                                    .and_then(|res| match &res.content {
                                        Some(Loadable::Ready(streams)) => {
                                            let stream = streams
                                                .iter()
                                                .find(|s| s.is_source_match(&stream_item.stream))
                                                .or_else(|| {
                                                    streams
                                                        .iter()
                                                        .find(|s| s.is_binge_match(&stream_item.stream))
                                                })
                                                .cloned();
                                            
                                            Some(ResourceLoadable {
                                                request: res.request.clone(),
                                                content: Some(Loadable::Ready(stream)),
                                            })
                                        }
                                        _ => None,
                                    })
                            })
                            .or_else(|| {
                                Some(ResourceLoadable {
                                    request: meta_item.request.clone(),
                                    content: Some(Loadable::Ready(None)),
                                })
                            })
                    }
                    _ => None,
                })
        }
        _ => None,
    };
    
    eq_update(last_used_stream, next_stream)
}

Library Integration

Library Item

Creates or updates LibraryItem from metadata:
fn library_item_update<E: Env + 'static>(
    library_item: &mut Option<LibraryItem>,
    selected: &Option<Selected>,
    meta_items: &[ResourceLoadable<MetaItem>],
    library: &LibraryBucket,
) -> Effects {
    let meta_item = meta_items
        .iter()
        .find_map(|item| match &item.content {
            Some(Loadable::Ready(meta)) => Some(meta),
            _ => None,
        });
    
    let next_item = match selected {
        Some(selected) => {
            library.items
                .get(&selected.meta_path.id)
                .map(|lib_item| {
                    meta_item.map_or_else(
                        || lib_item.to_owned(),
                        |meta| LibraryItem::from((&meta.preview, lib_item)),
                    )
                })
                .or_else(|| {
                    meta_item.map(|meta| {
                        LibraryItem::from((&meta.preview, PhantomData::<E>))
                    })
                })
        }
        _ => None,
    };
    
    eq_update(library_item, next_item)
}

Watched BitField

Tracks which episodes have been watched:
fn watched_update(
    watched: &mut Option<WatchedBitField>,
    meta_items: &[ResourceLoadable<MetaItem>],
    library_item: &Option<LibraryItem>,
) -> Effects {
    let next_watched = meta_items
        .iter()
        .find_map(|item| match &item.content {
            Some(Loadable::Ready(meta)) => Some(meta),
            _ => None,
        })
        .and_then(|meta| {
            library_item
                .as_ref()
                .map(|lib_item| (meta, lib_item))
        })
        .map(|(meta, lib_item)| {
            lib_item.state.watched_bitfield(&meta.videos)
        });
    
    eq_update(watched, next_watched)
}

Rating System

Supported Items

const USER_LIKES_SUPPORTED_ID_PREFIXES: [&str; 2] = ["tt", "kitsu:"];
const USER_LIKES_SUPPORTED_TYPES: [&str; 2] = ["movie", "series"];

fn supported_rating_id(id: &str) -> bool {
    USER_LIKES_SUPPORTED_ID_PREFIXES
        .iter()
        .any(|prefix| id.starts_with(prefix))
}

Get Rating

fn get_rating<E: Env + 'static>(auth_key: AuthKey, meta_path: &ResourcePath) -> Effect {
    let request = RatingGetStatusRequest {
        auth_key,
        meta_item_id: meta_path.id.to_owned(),
        meta_item_type: meta_path.r#type.to_owned(),
    };
    
    EffectFuture::Concurrent(
        E::fetch::<_, RatingGetStatusResponse>(request.into())
            .map(move |result| {
                Msg::Internal(Internal::RatingGetStatusResult(
                    meta_path.id.clone(),
                    result,
                ))
            })
    )
}

Send Rating

pub struct RatingInfo {
    pub meta_id: String,
    pub status: Option<RatingStatus>,
}

pub enum Rating {
    Like,
    Dislike,
}
Action:
Msg::Action(Action::MetaDetails(ActionMetaDetails::Rate(Some(Rating::Like))))

Watch State Actions

Mark as Watched

Msg::Action(Action::MetaDetails(ActionMetaDetails::MarkAsWatched(true)))
Increments times_watched and updates last_watched.

Mark Video as Watched

Msg::Action(Action::MetaDetails(
    ActionMetaDetails::MarkVideoAsWatched(video, true)
))
Updates the WatchedBitField for specific episodes.

Mark Season as Watched

Msg::Action(Action::MetaDetails(
    ActionMetaDetails::MarkSeasonAsWatched(season_number, true)
))
Marks all episodes in a season as watched.

Usage Example

use stremio_core::models::meta_details::{MetaDetails, Selected};
use stremio_core::types::addon::ResourcePath;

// Load meta details for a movie
let selected = Selected {
    meta_path: ResourcePath {
        resource: "meta".to_string(),
        r#type: "movie".to_string(),
        id: "tt1254207".to_string(),  // Big Fish
        extra: vec![],
    },
    stream_path: Some(ResourcePath {
        resource: "stream".to_string(),
        r#type: "movie".to_string(),
        id: "tt1254207".to_string(),
        extra: vec![],
    }),
    guess_stream: false,
};

runtime.dispatch(Msg::Action(
    Action::Load(ActionLoad::MetaDetails(selected))
));

// Wait for meta items to load
// Then access:
// - meta_details.meta_items - Metadata from all addons
// - meta_details.streams - Available streams
// - meta_details.library_item - Library state
// - meta_details.last_used_stream - Suggested stream for binge watching

// Rate the item
if meta_details.rating_info.is_some() {
    runtime.dispatch(Msg::Action(
        Action::MetaDetails(ActionMetaDetails::Rate(Some(Rating::Like)))
    ));
}

Best Practices

Use guess_stream: true for series to automatically select the appropriate episode. For movies or when you know the specific video ID, set stream_path directly.
MetaDetails automatically syncs library item metadata on load, ensuring the LibraryItem is created or updated with latest info.
The last_used_stream field provides continuity for binge watching by matching streams based on source and binge group hints.

Build docs developers (and LLMs) love