Skip to main content

crispy_stalker/
types.rs

1//! Stalker portal domain types.
2//!
3//! These are the protocol-native representations returned by the Stalker
4//! middleware API. Consumers implement `From<StalkerChannel>` etc. to map
5//! into app-specific models.
6
7use serde::{Deserialize, Serialize};
8
9/// Credentials required to connect to a Stalker portal.
10#[derive(Clone, Serialize, Deserialize)]
11pub struct StalkerCredentials {
12    /// Base URL of the portal (e.g. `http://portal.example.com`).
13    pub base_url: String,
14
15    /// MAC address in `XX:XX:XX:XX:XX:XX` format.
16    pub mac_address: String,
17
18    /// Timezone for cookie header (e.g. `Europe/Paris`).
19    /// Defaults to `Europe/Paris` if `None`.
20    #[serde(default)]
21    pub timezone: Option<String>,
22}
23
24impl std::fmt::Debug for StalkerCredentials {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("StalkerCredentials")
27            .field("base_url", &self.base_url)
28            .field("mac_address", &"[REDACTED]")
29            .finish()
30    }
31}
32
33/// A live TV channel from the Stalker portal.
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35pub struct StalkerChannel {
36    /// Portal-internal channel ID.
37    pub id: String,
38
39    /// Display name.
40    pub name: String,
41
42    /// Channel number / LCN.
43    pub number: Option<u32>,
44
45    /// Raw stream command (may need resolution via `resolve_stream_url`).
46    pub cmd: String,
47
48    /// Genre / category ID on the portal.
49    pub tv_genre_id: Option<String>,
50
51    /// Channel logo URL.
52    pub logo: Option<String>,
53
54    /// EPG channel identifier for programme matching.
55    pub epg_channel_id: Option<String>,
56
57    /// Whether catch-up / archive is available.
58    #[serde(default)]
59    pub has_archive: bool,
60
61    /// Number of archive days available.
62    #[serde(default)]
63    pub archive_days: u32,
64
65    /// Whether the channel is marked as adult / censored.
66    #[serde(default)]
67    pub is_censored: bool,
68}
69
70/// A VOD (movie) item from the Stalker portal.
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72pub struct StalkerVodItem {
73    /// Portal-internal VOD ID.
74    pub id: String,
75
76    /// Display name.
77    pub name: String,
78
79    /// Raw stream command.
80    pub cmd: String,
81
82    /// Category ID on the portal.
83    pub category_id: Option<String>,
84
85    /// Poster / cover image URL.
86    pub logo: Option<String>,
87
88    /// Plot summary.
89    pub description: Option<String>,
90
91    /// Release year.
92    pub year: Option<String>,
93
94    /// Genre string.
95    pub genre: Option<String>,
96
97    /// Rating string (e.g. "7.5").
98    pub rating: Option<String>,
99
100    /// Director name(s).
101    pub director: Option<String>,
102
103    /// Cast members.
104    pub cast: Option<String>,
105
106    /// Duration string (e.g. "01:45:00").
107    pub duration: Option<String>,
108
109    /// TMDB ID for metadata enrichment.
110    pub tmdb_id: Option<i64>,
111}
112
113/// A series item from the Stalker portal.
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
115pub struct StalkerSeriesItem {
116    /// Portal-internal series ID.
117    pub id: String,
118
119    /// Display name.
120    pub name: String,
121
122    /// Category ID on the portal.
123    pub category_id: Option<String>,
124
125    /// Poster / cover image URL.
126    pub logo: Option<String>,
127
128    /// Plot summary.
129    pub description: Option<String>,
130
131    /// Release year.
132    pub year: Option<String>,
133
134    /// Genre string.
135    pub genre: Option<String>,
136
137    /// Rating string.
138    pub rating: Option<String>,
139
140    /// Director name(s).
141    pub director: Option<String>,
142
143    /// Cast members.
144    pub cast: Option<String>,
145}
146
147/// A season within a series from the Stalker portal.
148///
149/// Translated from Python `fetch_season_pages` / TypeScript `getSeasons`.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct StalkerSeason {
152    /// Portal-internal season ID.
153    pub id: String,
154
155    /// Display name (e.g. "Season 1").
156    pub name: String,
157
158    /// Parent movie/series ID.
159    pub movie_id: String,
160
161    /// Poster / cover image URL.
162    pub logo: Option<String>,
163
164    /// Plot summary.
165    pub description: Option<String>,
166}
167
168/// An episode within a season from the Stalker portal.
169///
170/// Translated from Python `fetch_episode_pages` / TypeScript `getEpisodes`.
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172pub struct StalkerEpisode {
173    /// Portal-internal episode ID.
174    pub id: String,
175
176    /// Display name (e.g. "Episode 1").
177    pub name: String,
178
179    /// Parent movie/series ID.
180    pub movie_id: String,
181
182    /// Parent season ID.
183    pub season_id: String,
184
185    /// Episode number within the season.
186    pub episode_number: Option<u32>,
187
188    /// Raw stream command.
189    pub cmd: String,
190
191    /// Poster / cover image URL.
192    pub logo: Option<String>,
193
194    /// Plot summary.
195    pub description: Option<String>,
196
197    /// Duration string.
198    pub duration: Option<String>,
199}
200
201/// An EPG (Electronic Programme Guide) entry from the Stalker portal.
202///
203/// Translated from Python `Epg.py` normalization logic.
204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
205pub struct StalkerEpgEntry {
206    /// Programme name.
207    pub name: String,
208
209    /// Start timestamp (epoch seconds).
210    pub start_timestamp: Option<i64>,
211
212    /// End timestamp (epoch seconds).
213    pub end_timestamp: Option<i64>,
214
215    /// Programme description.
216    pub description: Option<String>,
217
218    /// Category / genre.
219    pub category: Option<String>,
220
221    /// Duration in seconds.
222    pub duration: Option<i64>,
223}
224
225/// A content category from the Stalker portal.
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct StalkerCategory {
228    /// Category ID.
229    pub id: String,
230
231    /// Display title.
232    pub title: String,
233
234    /// Whether this category contains adult content.
235    #[serde(default)]
236    pub is_adult: bool,
237}
238
239/// A page of results from a paginated Stalker API endpoint.
240#[derive(Debug, Clone)]
241pub struct PaginatedResult<T> {
242    /// Items on this page.
243    pub items: Vec<T>,
244
245    /// Total number of items across all pages.
246    pub total_items: u32,
247
248    /// Maximum items per page (server-determined).
249    pub max_page_items: u32,
250}
251
252impl<T> PaginatedResult<T> {
253    /// Total number of pages.
254    pub fn total_pages(&self) -> u32 {
255        if self.max_page_items == 0 {
256            return 1;
257        }
258        self.total_items.div_ceil(self.max_page_items)
259    }
260}
261
262/// Account information returned by `get_account_info`.
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
264pub struct StalkerAccountInfo {
265    /// Login identifier.
266    pub login: Option<String>,
267
268    /// MAC address on the account.
269    pub mac: Option<String>,
270
271    /// Account status (active, blocked, etc.).
272    pub status: Option<String>,
273
274    /// Subscription expiration date string.
275    pub expiration: Option<String>,
276
277    /// Subscription end date from `subscribed_till` field.
278    pub subscribed_till: Option<String>,
279}
280
281/// Full series detail: the series metadata plus all seasons and episodes.
282///
283/// Returned by [`StalkerClient::get_series_info`](crate::StalkerClient::get_series_info).
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
285pub struct StalkerSeriesDetail {
286    /// The series item metadata.
287    pub series: StalkerSeriesItem,
288
289    /// All seasons for the series.
290    pub seasons: Vec<StalkerSeason>,
291
292    /// Episodes keyed by season ID.
293    pub episodes: std::collections::HashMap<String, Vec<StalkerEpisode>>,
294}
295
296/// Profile information returned by `get_profile`.
297#[derive(Debug, Clone, Default, Serialize, Deserialize)]
298pub struct StalkerProfile {
299    /// Timezone setting.
300    pub timezone: Option<String>,
301
302    /// Locale / language.
303    pub locale: Option<String>,
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn credentials_debug_redacts_mac() {
312        let creds = StalkerCredentials {
313            base_url: "http://example.com".into(),
314            mac_address: "00:1A:79:AB:CD:EF".into(),
315            timezone: None,
316        };
317        let debug = format!("{creds:?}");
318        assert!(debug.contains("[REDACTED]"));
319        assert!(!debug.contains("00:1A:79"));
320    }
321
322    #[test]
323    fn paginated_result_total_pages() {
324        let result: PaginatedResult<()> = PaginatedResult {
325            items: vec![],
326            total_items: 25,
327            max_page_items: 10,
328        };
329        assert_eq!(result.total_pages(), 3);
330    }
331
332    #[test]
333    fn paginated_result_exact_division() {
334        let result: PaginatedResult<()> = PaginatedResult {
335            items: vec![],
336            total_items: 20,
337            max_page_items: 10,
338        };
339        assert_eq!(result.total_pages(), 2);
340    }
341
342    #[test]
343    fn paginated_result_zero_max_page_items() {
344        let result: PaginatedResult<()> = PaginatedResult {
345            items: vec![],
346            total_items: 5,
347            max_page_items: 0,
348        };
349        assert_eq!(result.total_pages(), 1);
350    }
351}