pub struct OnlineRepository {
http_client: Arc<HttpClient>,
server_url: String,
user_id: String,
access_token: String,
connectivity: Option<ConnectivityReporter>,
}Expand description
Online repository - fetches data from Jellyfin server via HTTP
Fields§
§http_client: Arc<HttpClient>§server_url: String§user_id: String§access_token: String§connectivity: Option<ConnectivityReporter>Reports the outcome of every server request to the connectivity monitor.
This is the source of truth for the offline/online banner. None in
tests / contexts where connectivity tracking isn’t wired up.
Implementations§
Source§impl OnlineRepository
impl OnlineRepository
Sourcepub fn user_id(&self) -> &str
pub fn user_id(&self) -> &str
The signed-in user these requests are made as. Needed by the favourites drain, which reads this user’s queued rows. TRACES: UR-069 | DR-120
pub fn new( http_client: Arc<HttpClient>, server_url: String, user_id: String, access_token: String, ) -> Self
Sourcepub fn with_connectivity(self, reporter: ConnectivityReporter) -> Self
pub fn with_connectivity(self, reporter: ConnectivityReporter) -> Self
Attach a connectivity reporter so server outcomes drive the reachability
state observed by the UI. See report_outcome.
Sourceasync fn report_outcome<T>(&self, result: &Result<T, RepoError>)
async fn report_outcome<T>(&self, result: &Result<T, RepoError>)
Feed a request outcome into the connectivity monitor.
Classification (matches docs/architecture/07-connectivity.md):
Ok/Authentication/NotFound/Server→ the server answered, so it is reachable →report_success(instant recovery).Network→ connection-level failure →report_network_failure(subject to the time-window debounce before going offline).Database→ not a server signal → ignored.
Sourcefn auth_header(&self) -> String
fn auth_header(&self) -> String
Build authorization header
Sourcepub async fn download_bytes(&self, url: &str) -> Result<Vec<u8>, String>
pub async fn download_bytes(&self, url: &str) -> Result<Vec<u8>, String>
Download raw bytes from a URL using the shared authenticated HTTP client. Used by thumbnail cache to download images with proper auth and connection reuse.
Sourcepub async fn get_jray_actors(
&self,
item_id: &str,
t: f64,
) -> Result<Vec<JRayActor>, RepoError>
pub async fn get_jray_actors( &self, item_id: &str, t: f64, ) -> Result<Vec<JRayActor>, RepoError>
Query the JRay plugin for the actors on screen at time t (seconds) in
the given item. Returns an empty list when the plugin isn’t installed or
has no truth data for the item (HTTP 404), so callers can treat “no JRay”
and “nobody on screen” identically. Other failures propagate.
Sourceasync fn get_json<T: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
) -> Result<T, RepoError>
async fn get_json<T: for<'de> Deserialize<'de>>( &self, endpoint: &str, ) -> Result<T, RepoError>
Make authenticated GET request
async fn get_json_inner<T: for<'de> Deserialize<'de>>( &self, endpoint: &str, ) -> Result<T, RepoError>
Sourceasync fn post_json<T: Serialize>(
&self,
endpoint: &str,
body: &T,
) -> Result<(), RepoError>
async fn post_json<T: Serialize>( &self, endpoint: &str, body: &T, ) -> Result<(), RepoError>
Make authenticated POST request
async fn post_json_inner<T: Serialize>( &self, endpoint: &str, body: &T, ) -> Result<(), RepoError>
Sourceasync fn post_json_response<T: Serialize, R: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
body: &T,
) -> Result<R, RepoError>
async fn post_json_response<T: Serialize, R: for<'de> Deserialize<'de>>( &self, endpoint: &str, body: &T, ) -> Result<R, RepoError>
Make authenticated POST request and return response
async fn post_json_response_inner<T: Serialize, R: for<'de> Deserialize<'de>>( &self, endpoint: &str, body: &T, ) -> Result<R, RepoError>
Sourceasync fn stop_transcode(&self, play_session_id: &str)
async fn stop_transcode(&self, play_session_id: &str)
Ask the server to tear down a transcode this device started.
Best-effort and deliberately un-retried: it runs on the path that opens a replacement stream, so a slow or failed stop must not delay playback. The worst case if it does fail is the job Jellyfin would have reaped on its own idle timer anyway — the new stream still has its own session id, so it no longer collides with the old one.
TRACES: UR-074 | DR-177
Sourcepub async fn get_video_stream_url(
&self,
item_id: &str,
media_source_id: Option<&str>,
audio_stream_index: Option<i32>,
) -> Result<String, RepoError>
pub async fn get_video_stream_url( &self, item_id: &str, media_source_id: Option<&str>, audio_stream_index: Option<i32>, ) -> Result<String, RepoError>
Get a video stream URL (initial play, resume, transcoded seeking, audio-track switching).
Returns an HLS master playlist (/Videos/{id}/master.m3u8) transcoded to
h264/aac. HLS is used rather than a progressive stream.mp4 because the
HTML5 <video> element (via HLS.js) starts playing within seconds and can
seek within the stream, whereas a progressive MP4 transcode of HEVC source
forces the server to transcode the whole file before playback can begin —
which manifests as playback never starting.
There is deliberately no start-position parameter. A playlist covers
the whole item and asking for segment N is the seek, so a position would
be redundant — and actively fatal: Jellyfin builds every segment URI by
echoing this playlist’s query string into it, while its segment handler
rejects StartTimeTicks > 0 outright (ArgumentException → 400). One
resume position here therefore 400s every segment of the stream, which
presents as a resumed episode that simply never plays while the same
episode from the beginning is fine. Resume by seeking the player once it
has loaded. (The progressive /Audio/universal builder below has no
segments and keeps its StartTimeTicks.)
The stream is built against the current streaming_quality ceiling:
MaxStreamingBitrate/VideoBitrate/AudioBitrate, plus a MaxHeight
that suits the budget. Original keeps the historical 20/18 Mbps
allowance, which is a transcode ceiling rather than a user-facing limit.
TRACES: UR-004, UR-074 | DR-140, DR-162, DR-177, DR-181 | UT-130, UT-156, UT-173, UT-182
Sourcepub async fn build_audio_only_stream_url_for_video(
&self,
item_id: &str,
media_source_id: Option<&str>,
start_time_seconds: Option<f64>,
audio_stream_index: Option<i32>,
) -> Result<String, RepoError>
pub async fn build_audio_only_stream_url_for_video( &self, item_id: &str, media_source_id: Option<&str>, start_time_seconds: Option<f64>, audio_stream_index: Option<i32>, ) -> Result<String, RepoError>
Get an audio-only stream URL for a video item, for the background-audio handoff (UR-040).
TRACES: UR-040 | JA-032, DR-140 | UT-059, UT-130
This deliberately targets /Audio/{id}/universal, NOT the video stream:
the server extracts/transcodes only the item’s audio track and streams
pure audio bytes — no video frames reach the device, so there is no client
video decode while backgrounded. Do NOT “optimize” this to reuse the
/Videos/.../master.m3u8 URL: that would keep the device decoding video,
defeating the entire point of the feature.
AudioStreamIndex carries the user’s currently-selected audio track over
from the video player; StartTimeTicks resumes at the handoff position.
universal lets the server pick direct-play vs transcode per codec/device.
The stream is a progressive container (mp3 over plain HTTP), NOT HLS:
ExoPlayer plays this natively, whereas an HLS/ts transcode on the
/universal endpoint (no .m3u8 in the path) fails its progressive
loader with ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED. mp3 is universally
decodable and supports mid-stream StartTimeTicks.
Trait Implementations§
Source§impl MediaRepository for OnlineRepository
impl MediaRepository for OnlineRepository
Source§fn get_item<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<MediaItem, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_item<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<MediaItem, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Fetch one item with every field the detail and player screens need.
The Fields= list is the load-bearing part: Jellyfin omits these unless
they are named. MediaStreams is what makes the item’s audio and
subtitle tracks knowable at all — there is no separate “tracks”
endpoint, so this single call is how the player learns which audio tracks
an item offers (to_media_item maps them, and the player’s selector
filters them by kind). People is likewise how cast and crew are
obtained.
TRACES: UR-021, UR-035 | IR-016, IR-022, JA-005, JA-009
Source§fn get_resume_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_resume_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Continue Watching: the items this user has started and not finished.
/Users/{uid}/Items/Resume is the server-side answer to both “what goes
in the Continue Watching row” and “where was this left off” — each item
carries its own UserData.PlaybackPositionTicks, which is why UserData
is named in Fields= rather than left to the server’s default field set.
TRACES: UR-019, UR-023 | IR-024, JA-013, JA-015
Source§fn get_next_up_episodes<'life0, 'life1, 'async_trait>(
&'life0 self,
series_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_next_up_episodes<'life0, 'life1, 'async_trait>(
&'life0 self,
series_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
“Next Up”: the episode that follows the ones this user has finished, per series — the Shows-scoped counterpart to Continue Watching.
TRACES: UR-023, UR-059 | IR-024, JA-014
Source§fn get_resume_movies<'life0, 'async_trait>(
&'life0 self,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_resume_movies<'life0, 'async_trait>(
&'life0 self,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Continue Watching, narrowed to movies — the home screen’s movie row and the movie library’s own hero both want the unfinished films without the episodes mixed in.
TRACES: UR-019, UR-034 | IR-024, JA-013, JA-015
Source§fn search<'life0, 'life1, 'async_trait>(
&'life0 self,
query: &'life1 str,
options: Option<SearchOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn search<'life0, 'life1, 'async_trait>(
&'life0 self,
query: &'life1 str,
options: Option<SearchOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Search every library the user can see.
Recursive=true with no ParentId is what makes this cross-library
rather than folder-scoped; a caller narrowing the search passes the item
types through SearchOptions (already expanded from an opaque
SearchScope on this side of the boundary).
TRACES: UR-008 | IR-010, JA-006
Source§fn get_video_download_url(
&self,
item_id: &str,
quality: &str,
media_source_id: Option<&str>,
source_audio_codec: Option<&str>,
) -> String
fn get_video_download_url( &self, item_id: &str, quality: &str, media_source_id: Option<&str>, source_audio_codec: Option<&str>, ) -> String
TRACES: UR-071 | DR-123
Source§fn get_favorites<'life0, 'async_trait>(
&'life0 self,
scope: SearchScope,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_favorites<'life0, 'async_trait>(
&'life0 self,
scope: SearchScope,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
TRACES: UR-067 | DR-115, JA-033 | UT-100
Source§fn unmark_favorite<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn unmark_favorite<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Un-favourite an item: the same /Users/{uid}/FavoriteItems/{id} resource
as Self::mark_favorite, removed rather than posted. Written out by
hand rather than through post_json because it is the one favourite call
that needs DELETE.
TRACES: UR-017 | JA-018, DR-021
Source§fn clear_watch_history<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn clear_watch_history<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
DELETE /Users/{userId}/PlayedItems/{itemId} — Jellyfin’s “mark
unplayed”, which also zeroes the resume position. On a folder (series,
season) the server applies it recursively to the children.
TRACES: UR-064 | DR-106, JA-033
Source§fn mark_played<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn mark_played<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
POST /Users/{userId}/PlayedItems/{itemId} — the mirror image of
clear_watch_history.
TRACES: UR-025 | DR-131 | JA-035
Source§fn get_person<'life0, 'life1, 'async_trait>(
&'life0 self,
person_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<MediaItem, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_person<'life0, 'life1, 'async_trait>(
&'life0 self,
person_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<MediaItem, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
A single Person item (actor, director, …) by id.
Jellyfin models people as ordinary items, so this is the plain item
endpoint rather than anything under /Persons; the cast entries returned
on an item’s People field carry the ids this is called with.
TRACES: UR-035, UR-036 | IR-022, JA-030
Source§fn get_items_by_person<'life0, 'life1, 'async_trait>(
&'life0 self,
person_id: &'life1 str,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_items_by_person<'life0, 'life1, 'async_trait>(
&'life0 self,
person_id: &'life1 str,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
A person’s filmography — every item they are credited on.
TRACES: UR-036 | IR-022, JA-031
Source§fn get_libraries<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Library>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_libraries<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<Library>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn get_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: &'life1 str,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: &'life1 str,
options: Option<GetItemsOptions>,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_latest_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: &'life1 str,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_latest_items<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: &'life1 str,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_recently_played_audio<'life0, 'async_trait>(
&'life0 self,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_recently_played_audio<'life0, 'async_trait>(
&'life0 self,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn get_rediscover_albums<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_rediscover_albums<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
limit: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_genres<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Genre>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_genres<'life0, 'life1, 'async_trait>(
&'life0 self,
parent_id: Option<&'life1 str>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Genre>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_playback_info<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<PlaybackInfo, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_playback_info<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<PlaybackInfo, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_audio_stream_url<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<String, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn get_audio_stream_url<'life0, 'life1, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<String, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Source§fn get_audio_only_stream_url_for_video<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
media_source_id: Option<&'life2 str>,
start_time_seconds: Option<f64>,
audio_stream_index: Option<i32>,
) -> Pin<Box<dyn Future<Output = Result<String, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn get_audio_only_stream_url_for_video<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
item_id: &'life1 str,
media_source_id: Option<&'life2 str>,
start_time_seconds: Option<f64>,
audio_stream_index: Option<i32>,
) -> Pin<Box<dyn Future<Output = Result<String, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Source§fn get_live_tv_channels<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_live_tv_channels<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<Vec<MediaItem>, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Source§fn get_channels<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_channels<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = Result<SearchResult, RepoError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
get_items(channel_id, ...).