Skip to main content

crispy_xtream/
types.rs

1//! Xtream Codes API response types.
2//!
3//! These types mirror the JSON responses from Xtream-compatible servers.
4//! Fields use `serde(alias)` to handle both `snake_case` and `camelCase` variants
5//! that different server implementations may return.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11// ---------------------------------------------------------------------------
12// Profile
13// ---------------------------------------------------------------------------
14
15/// Top-level profile response from `player_api.php` (no action or `get_profile`).
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct XtreamProfile {
18    pub user_info: XtreamUserProfile,
19    pub server_info: XtreamServerInfo,
20}
21
22/// User account information.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct XtreamUserProfile {
25    #[serde(default)]
26    pub username: String,
27
28    #[serde(default)]
29    pub password: String,
30
31    /// Server message (MOTD, notices).
32    #[serde(default)]
33    pub message: String,
34
35    /// Authentication flag (1 = authenticated).
36    #[serde(default)]
37    pub auth: u8,
38
39    /// Account status (e.g. "Active", "Disabled").
40    #[serde(default)]
41    pub status: String,
42
43    /// Expiration date as a Unix timestamp string.
44    #[serde(default)]
45    pub exp_date: Option<String>,
46
47    /// Whether this is a trial account ("0" or "1").
48    #[serde(default)]
49    pub is_trial: Option<String>,
50
51    /// Number of currently active connections.
52    #[serde(default)]
53    pub active_cons: Option<serde_json::Value>,
54
55    /// Account creation date.
56    #[serde(default)]
57    pub created_at: Option<String>,
58
59    /// Maximum concurrent connections (string in many implementations).
60    #[serde(default)]
61    pub max_connections: Option<serde_json::Value>,
62
63    /// Formats the user is allowed to access (e.g. `["ts", "m3u8"]`).
64    #[serde(default)]
65    pub allowed_output_formats: Vec<String>,
66}
67
68/// Server information.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct XtreamServerInfo {
71    /// Whether this is a XUI instance.
72    #[serde(default)]
73    pub xui: Option<serde_json::Value>,
74
75    /// Software version.
76    #[serde(default)]
77    pub version: Option<String>,
78
79    /// Software revision.
80    #[serde(default)]
81    pub revision: Option<String>,
82
83    /// Base URL of the server.
84    #[serde(default)]
85    pub url: Option<String>,
86
87    /// HTTP port.
88    #[serde(default)]
89    pub port: Option<serde_json::Value>,
90
91    /// HTTPS port.
92    #[serde(default)]
93    pub https_port: Option<serde_json::Value>,
94
95    /// Server protocol (http / https).
96    #[serde(default)]
97    pub server_protocol: Option<String>,
98
99    /// RTMP port.
100    #[serde(default)]
101    pub rtmp_port: Option<serde_json::Value>,
102
103    /// Server timezone.
104    #[serde(default)]
105    pub timezone: Option<String>,
106
107    /// Current server timestamp.
108    #[serde(default)]
109    pub timestamp_now: Option<i64>,
110
111    /// Current server time as formatted string.
112    #[serde(default)]
113    pub time_now: Option<String>,
114}
115
116// ---------------------------------------------------------------------------
117// Categories
118// ---------------------------------------------------------------------------
119
120/// A content category (live, VOD, or series).
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct XtreamCategory {
123    #[serde(default)]
124    pub category_id: String,
125
126    #[serde(default)]
127    pub category_name: String,
128
129    #[serde(default)]
130    pub parent_id: Option<serde_json::Value>,
131}
132
133// ---------------------------------------------------------------------------
134// Live Channels
135// ---------------------------------------------------------------------------
136
137/// A live TV channel from `get_live_streams`.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct XtreamChannel {
140    /// Position / order number.
141    #[serde(default)]
142    pub num: Option<i64>,
143
144    /// Channel display name.
145    #[serde(default)]
146    pub name: String,
147
148    /// Stream type (e.g. "live").
149    #[serde(default)]
150    pub stream_type: Option<String>,
151
152    /// Unique stream identifier.
153    #[serde(default)]
154    pub stream_id: i64,
155
156    /// Channel logo URL.
157    #[serde(default)]
158    pub stream_icon: Option<String>,
159
160    /// Thumbnail URL.
161    #[serde(default)]
162    pub thumbnail: Option<String>,
163
164    /// EPG channel identifier.
165    #[serde(default)]
166    pub epg_channel_id: Option<String>,
167
168    /// Date added (Unix timestamp string or formatted date).
169    #[serde(default)]
170    pub added: Option<String>,
171
172    /// Primary category ID.
173    #[serde(default)]
174    pub category_id: Option<String>,
175
176    /// All category IDs this channel belongs to.
177    #[serde(default)]
178    pub category_ids: Vec<serde_json::Value>,
179
180    /// Custom SID.
181    #[serde(default)]
182    pub custom_sid: Option<String>,
183
184    /// Whether TV archive is available (0 or 1).
185    #[serde(default)]
186    pub tv_archive: Option<i64>,
187
188    /// Direct source URL.
189    #[serde(default)]
190    pub direct_source: Option<String>,
191
192    /// Days of archive available.
193    #[serde(default)]
194    pub tv_archive_duration: Option<i64>,
195
196    /// Whether this channel is flagged as adult content by the provider.
197    #[serde(default)]
198    pub is_adult: bool,
199
200    /// Generated stream URL (populated by the client).
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub url: Option<String>,
203}
204
205// ---------------------------------------------------------------------------
206// Movies (VOD)
207// ---------------------------------------------------------------------------
208
209/// A movie listing from `get_vod_streams`.
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct XtreamMovieListing {
212    #[serde(default)]
213    pub num: Option<i64>,
214
215    #[serde(default)]
216    pub name: String,
217
218    #[serde(default)]
219    pub year: Option<String>,
220
221    #[serde(default)]
222    pub title: Option<String>,
223
224    #[serde(default)]
225    pub stream_type: Option<String>,
226
227    #[serde(default)]
228    pub stream_id: i64,
229
230    #[serde(default)]
231    pub stream_icon: Option<String>,
232
233    #[serde(default)]
234    pub rating: Option<serde_json::Value>,
235
236    #[serde(default)]
237    pub rating_5based: Option<serde_json::Value>,
238
239    #[serde(default)]
240    pub genre: Option<String>,
241
242    #[serde(default)]
243    pub added: Option<String>,
244
245    #[serde(default)]
246    pub episode_run_time: Option<serde_json::Value>,
247
248    #[serde(default)]
249    pub category_id: Option<String>,
250
251    #[serde(default)]
252    pub category_ids: Vec<serde_json::Value>,
253
254    #[serde(default)]
255    pub container_extension: Option<String>,
256
257    #[serde(default)]
258    pub custom_sid: Option<serde_json::Value>,
259
260    #[serde(default)]
261    pub direct_source: Option<String>,
262
263    #[serde(default)]
264    pub release_date: Option<String>,
265
266    #[serde(default)]
267    pub cast: Option<String>,
268
269    #[serde(default)]
270    pub director: Option<String>,
271
272    #[serde(default)]
273    pub plot: Option<String>,
274
275    #[serde(default)]
276    pub youtube_trailer: Option<String>,
277
278    /// Generated stream URL (populated by the client).
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub url: Option<String>,
281}
282
283/// Detailed movie info from `get_vod_info`.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct XtreamMovie {
286    pub info: Option<XtreamMovieInfo>,
287    pub movie_data: Option<XtreamMovieData>,
288
289    /// Generated stream URL (populated by the client).
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub url: Option<String>,
292}
293
294/// Movie stream data.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct XtreamMovieData {
297    #[serde(default)]
298    pub stream_id: i64,
299
300    #[serde(default)]
301    pub name: Option<String>,
302
303    #[serde(default)]
304    pub title: Option<String>,
305
306    #[serde(default)]
307    pub year: Option<String>,
308
309    #[serde(default)]
310    pub added: Option<String>,
311
312    #[serde(default)]
313    pub category_id: Option<String>,
314
315    #[serde(default)]
316    pub category_ids: Vec<serde_json::Value>,
317
318    #[serde(default)]
319    pub container_extension: Option<String>,
320
321    #[serde(default)]
322    pub custom_sid: Option<String>,
323
324    #[serde(default)]
325    pub direct_source: Option<String>,
326}
327
328/// Extended movie metadata.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct XtreamMovieInfo {
331    #[serde(default)]
332    pub kinopoisk_url: Option<String>,
333
334    #[serde(default)]
335    pub tmdb_id: Option<serde_json::Value>,
336
337    #[serde(default)]
338    pub name: Option<String>,
339
340    #[serde(default, alias = "o_name")]
341    pub original_name: Option<String>,
342
343    #[serde(default)]
344    pub cover_big: Option<String>,
345
346    #[serde(default)]
347    pub movie_image: Option<String>,
348
349    #[serde(default)]
350    pub release_date: Option<String>,
351
352    #[serde(default)]
353    pub episode_run_time: Option<serde_json::Value>,
354
355    #[serde(default)]
356    pub youtube_trailer: Option<String>,
357
358    #[serde(default)]
359    pub director: Option<String>,
360
361    #[serde(default)]
362    pub actors: Option<String>,
363
364    #[serde(default)]
365    pub cast: Option<String>,
366
367    #[serde(default)]
368    pub description: Option<String>,
369
370    #[serde(default)]
371    pub plot: Option<String>,
372
373    #[serde(default)]
374    pub age: Option<String>,
375
376    #[serde(default)]
377    pub mpaa_rating: Option<String>,
378
379    #[serde(default)]
380    pub rating_count_kinopoisk: Option<serde_json::Value>,
381
382    #[serde(default)]
383    pub country: Option<String>,
384
385    #[serde(default)]
386    pub genre: Option<String>,
387
388    #[serde(default)]
389    pub backdrop_path: Option<serde_json::Value>,
390
391    #[serde(default)]
392    pub duration_secs: Option<i64>,
393
394    #[serde(default)]
395    pub duration: Option<String>,
396
397    #[serde(default)]
398    pub bitrate: Option<serde_json::Value>,
399
400    #[serde(default, alias = "releasedate")]
401    pub release_date_alt: Option<String>,
402
403    #[serde(default)]
404    pub subtitles: Option<serde_json::Value>,
405
406    #[serde(default)]
407    pub rating: Option<serde_json::Value>,
408}
409
410// ---------------------------------------------------------------------------
411// Series / Shows
412// ---------------------------------------------------------------------------
413
414/// A series listing from `get_series`.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct XtreamShowListing {
417    #[serde(default)]
418    pub num: Option<i64>,
419
420    #[serde(default)]
421    pub name: String,
422
423    #[serde(default)]
424    pub title: Option<String>,
425
426    #[serde(default)]
427    pub year: Option<String>,
428
429    #[serde(default)]
430    pub series_id: i64,
431
432    #[serde(default)]
433    pub stream_type: Option<String>,
434
435    #[serde(default)]
436    pub cover: Option<String>,
437
438    #[serde(default)]
439    pub plot: Option<String>,
440
441    #[serde(default)]
442    pub cast: Option<String>,
443
444    #[serde(default)]
445    pub director: Option<String>,
446
447    #[serde(default)]
448    pub genre: Option<String>,
449
450    #[serde(default, alias = "releaseDate")]
451    pub release_date: Option<String>,
452
453    #[serde(default)]
454    pub last_modified: Option<String>,
455
456    #[serde(default)]
457    pub rating: Option<serde_json::Value>,
458
459    #[serde(default)]
460    pub rating_5based: Option<serde_json::Value>,
461
462    #[serde(default)]
463    pub backdrop_path: Option<serde_json::Value>,
464
465    #[serde(default)]
466    pub youtube_trailer: Option<String>,
467
468    #[serde(default)]
469    pub episode_run_time: Option<serde_json::Value>,
470
471    #[serde(default)]
472    pub category_id: Option<String>,
473
474    #[serde(default)]
475    pub category_ids: Vec<serde_json::Value>,
476}
477
478/// Detailed series info from `get_series_info`.
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct XtreamShow {
481    #[serde(default)]
482    pub seasons: Vec<XtreamSeason>,
483
484    pub info: Option<XtreamShowInfo>,
485
486    /// Episodes grouped by season number (key is season number as string).
487    #[serde(default)]
488    pub episodes: HashMap<String, Vec<XtreamEpisode>>,
489}
490
491/// Series metadata.
492#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct XtreamShowInfo {
494    #[serde(default)]
495    pub name: Option<String>,
496
497    #[serde(default)]
498    pub title: Option<String>,
499
500    #[serde(default)]
501    pub year: Option<String>,
502
503    #[serde(default)]
504    pub series_id: Option<i64>,
505
506    #[serde(default)]
507    pub cover: Option<String>,
508
509    #[serde(default)]
510    pub plot: Option<String>,
511
512    #[serde(default)]
513    pub cast: Option<String>,
514
515    #[serde(default)]
516    pub director: Option<String>,
517
518    #[serde(default)]
519    pub genre: Option<String>,
520
521    #[serde(default, alias = "releaseDate")]
522    pub release_date: Option<String>,
523
524    #[serde(default)]
525    pub last_modified: Option<String>,
526
527    #[serde(default)]
528    pub rating: Option<serde_json::Value>,
529
530    #[serde(default)]
531    pub rating_5based: Option<serde_json::Value>,
532
533    #[serde(default)]
534    pub backdrop_path: Option<serde_json::Value>,
535
536    #[serde(default)]
537    pub youtube_trailer: Option<String>,
538
539    #[serde(default)]
540    pub episode_run_time: Option<serde_json::Value>,
541
542    #[serde(default)]
543    pub category_id: Option<String>,
544
545    #[serde(default)]
546    pub category_ids: Vec<serde_json::Value>,
547}
548
549/// A season within a series.
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct XtreamSeason {
552    #[serde(default)]
553    pub id: Option<i64>,
554
555    #[serde(default)]
556    pub name: Option<String>,
557
558    #[serde(default)]
559    pub episode_count: Option<i64>,
560
561    #[serde(default)]
562    pub overview: Option<String>,
563
564    #[serde(default)]
565    pub air_date: Option<String>,
566
567    #[serde(default)]
568    pub cover: Option<String>,
569
570    #[serde(default)]
571    pub season_number: Option<i64>,
572
573    #[serde(default)]
574    pub cover_big: Option<String>,
575
576    #[serde(default)]
577    pub vote_average: Option<f64>,
578}
579
580/// An episode within a series season.
581#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct XtreamEpisode {
583    #[serde(default)]
584    pub id: Option<serde_json::Value>,
585
586    #[serde(default)]
587    pub episode_num: Option<serde_json::Value>,
588
589    #[serde(default)]
590    pub title: Option<String>,
591
592    #[serde(default)]
593    pub container_extension: Option<String>,
594
595    #[serde(default)]
596    pub info: Option<XtreamEpisodeInfo>,
597
598    #[serde(default)]
599    pub custom_sid: Option<String>,
600
601    #[serde(default)]
602    pub added: Option<String>,
603
604    #[serde(default)]
605    pub season: Option<serde_json::Value>,
606
607    #[serde(default)]
608    pub direct_source: Option<String>,
609
610    #[serde(default)]
611    pub subtitles: Option<serde_json::Value>,
612
613    /// Generated stream URL (populated by the client).
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub url: Option<String>,
616}
617
618/// Episode metadata.
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct XtreamEpisodeInfo {
621    #[serde(default)]
622    pub air_date: Option<String>,
623
624    #[serde(default)]
625    pub release_date: Option<String>,
626
627    #[serde(default)]
628    pub plot: Option<String>,
629
630    #[serde(default)]
631    pub rating: Option<serde_json::Value>,
632
633    #[serde(default)]
634    pub movie_image: Option<String>,
635
636    #[serde(default)]
637    pub cover_big: Option<String>,
638
639    #[serde(default)]
640    pub duration_secs: Option<i64>,
641
642    #[serde(default)]
643    pub duration: Option<String>,
644
645    #[serde(default)]
646    pub tmdb_id: Option<serde_json::Value>,
647
648    #[serde(default)]
649    pub video: Option<serde_json::Value>,
650
651    #[serde(default)]
652    pub audio: Option<serde_json::Value>,
653
654    #[serde(default)]
655    pub bitrate: Option<serde_json::Value>,
656
657    #[serde(default)]
658    pub season: Option<serde_json::Value>,
659}
660
661// ---------------------------------------------------------------------------
662// EPG (Electronic Programme Guide)
663// ---------------------------------------------------------------------------
664
665/// Short EPG response from `get_short_epg`.
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct XtreamShortEpg {
668    #[serde(default)]
669    pub epg_listings: Vec<XtreamEpgListing>,
670}
671
672/// A single EPG listing entry.
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct XtreamEpgListing {
675    #[serde(default)]
676    pub id: Option<String>,
677
678    #[serde(default)]
679    pub epg_id: Option<String>,
680
681    /// Title — may be base64-encoded by the server.
682    #[serde(default)]
683    pub title: Option<String>,
684
685    #[serde(default)]
686    pub lang: Option<String>,
687
688    #[serde(default)]
689    pub start: Option<String>,
690
691    #[serde(default)]
692    pub end: Option<String>,
693
694    /// Description — may be base64-encoded by the server.
695    #[serde(default)]
696    pub description: Option<String>,
697
698    #[serde(default)]
699    pub channel_id: Option<String>,
700
701    #[serde(default)]
702    pub start_timestamp: Option<serde_json::Value>,
703
704    #[serde(default)]
705    pub stop_timestamp: Option<serde_json::Value>,
706
707    #[serde(default)]
708    pub stop: Option<String>,
709}
710
711/// Full EPG response from `get_simple_data_table`.
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct XtreamFullEpg {
714    #[serde(default)]
715    pub epg_listings: Vec<XtreamFullEpgListing>,
716}
717
718/// A full EPG listing entry with archive/playback flags.
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct XtreamFullEpgListing {
721    #[serde(default)]
722    pub id: Option<String>,
723
724    #[serde(default)]
725    pub epg_id: Option<String>,
726
727    /// Title — may be base64-encoded by the server.
728    #[serde(default)]
729    pub title: Option<String>,
730
731    #[serde(default)]
732    pub lang: Option<String>,
733
734    #[serde(default)]
735    pub start: Option<String>,
736
737    #[serde(default)]
738    pub end: Option<String>,
739
740    /// Description — may be base64-encoded by the server.
741    #[serde(default)]
742    pub description: Option<String>,
743
744    #[serde(default)]
745    pub channel_id: Option<String>,
746
747    #[serde(default)]
748    pub start_timestamp: Option<serde_json::Value>,
749
750    #[serde(default)]
751    pub stop_timestamp: Option<serde_json::Value>,
752
753    /// Whether this programme is currently playing (0 or 1).
754    #[serde(default)]
755    pub now_playing: Option<i64>,
756
757    /// Whether archive is available for this programme (0 or 1).
758    #[serde(default)]
759    pub has_archive: Option<i64>,
760}
761
762// ---------------------------------------------------------------------------
763// Stream format
764// ---------------------------------------------------------------------------
765
766/// Preferred stream output format.
767#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
768pub enum StreamFormat {
769    /// MPEG Transport Stream.
770    #[default]
771    Ts,
772    /// HTTP Live Streaming.
773    M3u8,
774    /// Real-Time Messaging Protocol.
775    Rtmp,
776}
777
778impl StreamFormat {
779    /// File extension for URL construction.
780    pub fn extension(self) -> &'static str {
781        match self {
782            Self::Ts => "ts",
783            Self::M3u8 => "m3u8",
784            Self::Rtmp => "ts", // RTMP falls back to TS for the URL path
785        }
786    }
787}
788
789impl std::fmt::Display for StreamFormat {
790    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791        f.write_str(self.extension())
792    }
793}
794
795/// Type of content for stream URL generation.
796#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
797pub enum StreamType {
798    /// Live TV channel.
799    Channel,
800    /// VOD movie.
801    Movie,
802    /// Series episode.
803    Episode,
804}
805
806impl StreamType {
807    /// URL path segment for this stream type.
808    pub fn path_segment(self) -> &'static str {
809        match self {
810            Self::Channel => "live",
811            Self::Movie => "movie",
812            Self::Episode => "series",
813        }
814    }
815}
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820
821    #[test]
822    fn stream_format_extensions() {
823        assert_eq!(StreamFormat::Ts.extension(), "ts");
824        assert_eq!(StreamFormat::M3u8.extension(), "m3u8");
825        assert_eq!(StreamFormat::Rtmp.extension(), "ts");
826    }
827
828    #[test]
829    fn stream_type_segments() {
830        assert_eq!(StreamType::Channel.path_segment(), "live");
831        assert_eq!(StreamType::Movie.path_segment(), "movie");
832        assert_eq!(StreamType::Episode.path_segment(), "series");
833    }
834
835    #[test]
836    fn deserialize_profile() {
837        let json = r#"{
838            "user_info": {
839                "username": "test",
840                "password": "pass",
841                "message": "",
842                "auth": 1,
843                "status": "Active",
844                "exp_date": "1735689600",
845                "is_trial": "0",
846                "active_cons": 0,
847                "created_at": "1704067200",
848                "max_connections": "1",
849                "allowed_output_formats": ["ts", "m3u8"]
850            },
851            "server_info": {
852                "xui": true,
853                "version": "1.5.12",
854                "revision": null,
855                "url": "example.com",
856                "port": "80",
857                "https_port": "443",
858                "server_protocol": "http",
859                "rtmp_port": "8880",
860                "timezone": "UTC",
861                "timestamp_now": 1704067200,
862                "time_now": "2024-01-01 00:00:00"
863            }
864        }"#;
865
866        let profile: XtreamProfile = serde_json::from_str(json).unwrap();
867        assert_eq!(profile.user_info.username, "test");
868        assert_eq!(profile.user_info.auth, 1);
869        assert_eq!(profile.user_info.status, "Active");
870        assert_eq!(profile.user_info.allowed_output_formats.len(), 2);
871        assert_eq!(profile.server_info.timezone.as_deref(), Some("UTC"));
872    }
873
874    #[test]
875    fn deserialize_category() {
876        let json = r#"{"category_id": "1", "category_name": "Sports", "parent_id": 0}"#;
877        let cat: XtreamCategory = serde_json::from_str(json).unwrap();
878        assert_eq!(cat.category_id, "1");
879        assert_eq!(cat.category_name, "Sports");
880    }
881
882    #[test]
883    fn deserialize_channel() {
884        let json = r#"{
885            "num": 1,
886            "name": "BBC One",
887            "stream_type": "live",
888            "stream_id": 42,
889            "stream_icon": "http://img.example.com/bbc1.png",
890            "thumbnail": "",
891            "epg_channel_id": "bbc1.uk",
892            "added": "1704067200",
893            "category_id": "1",
894            "category_ids": [1],
895            "custom_sid": "",
896            "tv_archive": 1,
897            "direct_source": "",
898            "tv_archive_duration": 7
899        }"#;
900
901        let ch: XtreamChannel = serde_json::from_str(json).unwrap();
902        assert_eq!(ch.stream_id, 42);
903        assert_eq!(ch.name, "BBC One");
904        assert_eq!(ch.epg_channel_id.as_deref(), Some("bbc1.uk"));
905        assert_eq!(ch.tv_archive, Some(1));
906    }
907
908    #[test]
909    fn deserialize_epg_listing() {
910        let json = r#"{
911            "id": "123",
912            "epg_id": "bbc1.uk",
913            "title": "TmV3cw==",
914            "lang": "en",
915            "start": "2024-01-01 10:00:00",
916            "end": "2024-01-01 11:00:00",
917            "description": "RGFpbHkgbmV3cw==",
918            "channel_id": "bbc1.uk",
919            "start_timestamp": "1704106800",
920            "stop_timestamp": "1704110400",
921            "stop": "2024-01-01 11:00:00"
922        }"#;
923
924        let listing: XtreamEpgListing = serde_json::from_str(json).unwrap();
925        assert_eq!(listing.id.as_deref(), Some("123"));
926        assert_eq!(listing.title.as_deref(), Some("TmV3cw=="));
927    }
928}