Skip to main content

animedb/provider/
mod.rs

1//! Remote provider trait and concrete provider implementations.
2//!
3//! # Architecture
4//!
5//! - [`Provider`] — the core trait; all providers implement this interface
6//! - [`FetchPage`] — the return type for paginated sync operations
7//! - Concrete providers: [`AniListProvider`], [`JikanProvider`], [`KitsuProvider`],
8//!   [`TvmazeProvider`], [`ImdbProvider`]
9//! - [`ProviderRegistry`] — runtime registry mapping [`SourceName`] to concrete instances
10//!
11//! # Adding a new provider
12//!
13//! 1. Create `src/provider/myprovider.rs` implementing [`Provider`]
14//! 2. Add `pub use myprovider::MyProvider;` to this module
15//! 3. Register it in [`default_registry`]
16//!
17//! No changes needed to `remote.rs`, `sync/service.rs`, or any other module.
18
19use std::time::Duration;
20
21use crate::error::{Error, Result};
22use crate::model::{
23    CanonicalEpisode, CanonicalMedia, MediaKind, SearchOptions, SourceName, SyncCursor, SyncRequest,
24};
25
26// ---------------------------------------------------------------------------
27// Core trait types
28// ---------------------------------------------------------------------------
29
30/// A page of results returned by a paginated provider fetch.
31#[derive(Debug, Clone)]
32pub struct FetchPage {
33    /// The media items on this page.
34    pub items: Vec<CanonicalMedia>,
35    /// Cursor for the next page. `None` when the provider has exhausted its dataset.
36    pub next_cursor: Option<SyncCursor>,
37}
38
39/// Trait every remote metadata provider must implement.
40///
41/// Each provider is responsible for:
42/// - identifying itself via [`source`](Provider::source)
43/// - declaring its minimum request interval via [`min_interval`](Provider::min_interval)
44/// - paginated bulk fetches via [`fetch_page`](Provider::fetch_page)
45/// - keyword search via [`search`](Provider::search)
46/// - single-item lookup via [`get_by_id`](Provider::get_by_id)
47///
48/// Providers are pure, stateless structs — they hold only the HTTP client and
49/// base URL, never any in-flight state. Rate limiting between page fetches
50/// is handled by [`SyncService`](crate::sync::SyncService).
51pub trait Provider: Send + Sync {
52    /// Returns the provider that this instance represents.
53    fn source(&self) -> SourceName;
54
55    /// Minimum wall-clock delay between successive requests to this provider.
56    ///
57    /// Used by [`SyncService`](crate::sync::SyncService) to respect rate limits.
58    /// Return `Duration::ZERO` for providers with no explicit rate limit.
59    fn min_interval(&self) -> Duration {
60        Duration::ZERO
61    }
62
63    /// Fetches one page of media records for a sync request.
64    ///
65    /// The implementation must handle pagination internally using the `cursor`
66    /// argument. When the cursor is `SyncCursor { page: 1 }` (the default), the
67    /// first page of the dataset should be returned. When `next_cursor` is `None`,
68    /// the provider has exhausted its dataset.
69    fn fetch_page(&self, request: &SyncRequest, cursor: SyncCursor) -> Result<FetchPage>;
70
71    /// Performs a free-text search query against the provider.
72    fn search(&self, query: &str, options: SearchOptions) -> Result<Vec<CanonicalMedia>>;
73
74    /// Fetches a single media record by its ID on the provider.
75    ///
76    /// Returns `None` if the ID does not exist on the provider. Returns an error
77    /// only on network or protocol failure.
78    fn get_by_id(&self, media_kind: MediaKind, source_id: &str) -> Result<Option<CanonicalMedia>>;
79
80    /// Fetches currently trending or popular media, ideal for seeding catalogs.
81    ///
82    /// Default implementation returns a validation error indicating unsupported.
83    fn fetch_trending(&self, _media_kind: MediaKind) -> Result<Vec<CanonicalMedia>> {
84        Err(Error::Validation(format!(
85            "{} does not support trending",
86            self.source()
87        )))
88    }
89
90    /// Fetches recommendations based on a given media item.
91    ///
92    /// Default implementation returns a validation error indicating unsupported.
93    fn fetch_recommendations(
94        &self,
95        _media_kind: MediaKind,
96        _source_id: &str,
97    ) -> Result<Vec<CanonicalMedia>> {
98        Err(Error::Validation(format!(
99            "{} does not support recommendations",
100            self.source()
101        )))
102    }
103
104    /// Fetches related media (sequels, prequels, spin-offs, etc.) for a given media item.
105    ///
106    /// Default implementation returns a validation error indicating unsupported.
107    fn fetch_related(
108        &self,
109        _media_kind: MediaKind,
110        _source_id: &str,
111    ) -> Result<Vec<CanonicalMedia>> {
112        Err(Error::Validation(format!(
113            "{} does not support relations",
114            self.source()
115        )))
116    }
117
118    /// Fetches episode metadata for a given media item.
119    ///
120    /// Not all providers expose episode data. Kitsu is the only built-in provider
121    /// that implements this method; all others return a validation error.
122    fn fetch_episodes(
123        &self,
124        _media_kind: MediaKind,
125        _source_id: &str,
126    ) -> Result<Vec<CanonicalEpisode>> {
127        Err(Error::Validation(format!(
128            "{} does not support episode metadata",
129            self.source()
130        )))
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Submodules — one file per provider, shared utilities isolated in `http`
136// ---------------------------------------------------------------------------
137
138pub mod anilist;
139pub mod http;
140pub mod imdb;
141pub mod jikan;
142pub mod kitsu;
143pub mod registry;
144pub mod tvmaze;
145
146// ---------------------------------------------------------------------------
147// Public re-exports — only the provider structs, nothing internal
148// ---------------------------------------------------------------------------
149
150pub use anilist::AniListProvider;
151pub use imdb::ImdbProvider;
152pub use jikan::JikanProvider;
153pub use kitsu::KitsuProvider;
154pub use registry::{ProviderRegistry, default_registry};
155pub use tvmaze::TvmazeProvider;
156
157// Keep the old name alive as an alias so existing call sites don't break.
158#[deprecated(since = "0.3.0", note = "use `Provider` instead")]
159pub type RemoteProvider = dyn Provider;
160
161/// Compatibility alias — existing code that names `RemotePage` still compiles.
162#[deprecated(since = "0.3.0", note = "use `FetchPage` instead")]
163pub type RemotePage = FetchPage;