94 lines
3.2 KiB
Rust
94 lines
3.2 KiB
Rust
//! IPTV export: M3U playlist and XMLTV guide generation.
|
|
//!
|
|
//! Pure functions — no I/O, no dependencies beyond domain types.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::entities::{Channel, ScheduledSlot};
|
|
use crate::value_objects::ChannelId;
|
|
|
|
/// Generate an M3U playlist for the given channels.
|
|
///
|
|
/// Each entry points to the channel's `/stream` endpoint authenticated with the
|
|
/// provided JWT token so IPTV clients can load it directly.
|
|
pub fn generate_m3u(channels: &[Channel], base_url: &str, token: &str) -> String {
|
|
let mut out = String::from("#EXTM3U\n");
|
|
for ch in channels {
|
|
out.push_str(&format!(
|
|
"#EXTINF:-1 tvg-id=\"{}\" tvg-name=\"{}\" tvg-logo=\"\" group-title=\"K-TV\",{}\n",
|
|
ch.id, ch.name, ch.name
|
|
));
|
|
out.push_str(&format!(
|
|
"{}/api/v1/channels/{}/stream?token={}\n",
|
|
base_url, ch.id, token
|
|
));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Generate an XMLTV EPG document for the given channels and their scheduled slots.
|
|
pub fn generate_xmltv(
|
|
channels: &[Channel],
|
|
slots_by_channel: &HashMap<ChannelId, Vec<ScheduledSlot>>,
|
|
) -> String {
|
|
let mut out =
|
|
String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tv generator-info-name=\"k-tv\">\n");
|
|
|
|
for ch in channels {
|
|
out.push_str(&format!(
|
|
" <channel id=\"{}\"><display-name>{}</display-name></channel>\n",
|
|
ch.id,
|
|
escape_xml(&ch.name)
|
|
));
|
|
}
|
|
|
|
for ch in channels {
|
|
if let Some(slots) = slots_by_channel.get(&ch.id) {
|
|
for slot in slots {
|
|
let start = slot.start_at.format("%Y%m%d%H%M%S +0000");
|
|
let stop = slot.end_at.format("%Y%m%d%H%M%S +0000");
|
|
out.push_str(&format!(
|
|
" <programme start=\"{}\" stop=\"{}\" channel=\"{}\">\n",
|
|
start, stop, ch.id
|
|
));
|
|
out.push_str(&format!(
|
|
" <title lang=\"en\">{}</title>\n",
|
|
escape_xml(&slot.item.title)
|
|
));
|
|
if let Some(desc) = &slot.item.description {
|
|
out.push_str(&format!(
|
|
" <desc lang=\"en\">{}</desc>\n",
|
|
escape_xml(desc)
|
|
));
|
|
}
|
|
if let Some(genre) = slot.item.genres.first() {
|
|
out.push_str(&format!(
|
|
" <category lang=\"en\">{}</category>\n",
|
|
escape_xml(genre)
|
|
));
|
|
}
|
|
if let (Some(season), Some(episode)) =
|
|
(slot.item.season_number, slot.item.episode_number)
|
|
{
|
|
out.push_str(&format!(
|
|
" <episode-num system=\"onscreen\">S{}E{}</episode-num>\n",
|
|
season, episode
|
|
));
|
|
}
|
|
out.push_str(" </programme>\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
out.push_str("</tv>\n");
|
|
out
|
|
}
|
|
|
|
fn escape_xml(s: &str) -> String {
|
|
s.replace('&', "&")
|
|
.replace('<', "<")
|
|
.replace('>', ">")
|
|
.replace('"', """)
|
|
.replace('\'', "'")
|
|
}
|