use log::info;
use rss::{ChannelBuilder, ItemBuilder};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use crate::config::Marmite;
use crate::content::Content;
pub fn generate_rss(
contents: &[Content],
output_path: &Path,
filename: &str,
config: &Marmite,
) -> Result<(), String> {
let date_format = "%a, %d %b %Y %H:%M:%S GMT";
let feed_url = if !config.url.starts_with("http://") && !config.url.starts_with("https://") {
let protocol = if config.https.unwrap_or(false) {
"https://"
} else {
"http://"
};
format!("{}{}", protocol, &config.url)
} else {
config.url.clone()
};
let mut channel = ChannelBuilder::default()
.title(&config.name)
.link(&feed_url)
.description(&config.tagline)
.generator("marmite".to_string())
.build();
let filtered_contents: Vec<&Content> = contents
.iter()
.filter(|content| {
content
.stream
.as_ref()
.is_none_or(|stream| stream != "draft")
&& content.date.is_some() })
.collect();
for content in filtered_contents.iter().take(15) {
let content_date = content
.date
.expect("Content should have date - filtered above");
let mut item = ItemBuilder::default()
.title(content.title.clone())
.link(format!("{}/{}.html", &feed_url, &content.slug))
.description(content.description.clone())
.guid(
rss::GuidBuilder::default()
.value(format!("{}/{}.html", &feed_url, &content.slug))
.build(),
)
.pub_date(content_date.format(date_format).to_string())
.content(content.html.clone())
.source(
rss::SourceBuilder::default()
.url(&feed_url)
.title(filename.to_string())
.build(),
)
.build();
if let Some(author) = content.authors.first() {
item.author = Some(author.clone());
}
item.categories = content
.tags
.iter()
.map(|tag| rss::CategoryBuilder::default().name(tag.clone()).build())
.collect();
channel.items.push(item);
}
if let Some(latest_item) = channel.items.first() {
channel.pub_date = latest_item.pub_date.clone();
}
channel.last_build_date = Some(chrono::Utc::now().format(date_format).to_string());
if !config.card_image.is_empty() {
channel.image = Some(
rss::ImageBuilder::default()
.url(format!("{}/{}", &feed_url, &config.card_image))
.build(),
);
}
let rss = channel.to_string();
let feed_path = output_path.join(format!("{filename}.rss"));
let mut file = File::create(&feed_path).map_err(|e| e.to_string())?;
file.write_all(rss.as_bytes()).map_err(|e| e.to_string())?;
info!("Generated {}", &feed_path.display());
Ok(())
}
#[allow(clippy::module_name_repetitions)]
#[derive(Serialize, Deserialize, Debug)]
pub struct JsonFeed {
version: String,
title: String,
home_page_url: String,
feed_url: String,
description: String,
items: Vec<JsonFeedItem>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct JsonFeedItem {
id: String,
url: String,
title: String,
content_html: String,
summary: String,
date_published: String,
image: String,
authors: Vec<JsonFeedAuthor>,
tags: Vec<String>,
language: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct JsonFeedAuthor {
name: String,
url: String,
avatar: String,
}
pub fn generate_json(
contents: &[Content],
output_path: &Path,
filename: &str,
config: &Marmite,
) -> Result<(), String> {
let date_format = "%Y-%m-%dT%H:%M:%S-00:00"; let mut items = Vec::new();
let filtered_contents: Vec<&Content> = contents
.iter()
.filter(|content| {
content
.stream
.as_ref()
.is_none_or(|stream| stream != "draft")
&& content.date.is_some() })
.collect();
for content in filtered_contents.iter().take(15) {
let content_date = content
.date
.expect("Content should have date - filtered above");
let item = JsonFeedItem {
id: format!("{}/{}.html", &config.url, &content.slug),
url: format!("{}/{}.html", &config.url, &content.slug),
title: content.title.clone(),
content_html: content.html.clone(),
summary: content.description.clone().unwrap_or(String::new()),
date_published: content_date.format(date_format).to_string(),
image: content.card_image.clone().unwrap_or(String::new()),
authors: content
.authors
.iter()
.map(|author| {
if let Some(config_author) = config.authors.get(author) {
JsonFeedAuthor {
name: config_author.name.clone(),
url: {
if let Some(author_links) = &config_author.links {
author_links
.iter()
.next()
.map_or_else(String::new, |(_, url)| url.clone())
} else {
String::new()
}
},
avatar: config_author.avatar.clone().unwrap_or(String::new()),
}
} else {
JsonFeedAuthor {
name: author.clone(),
url: String::new(),
avatar: String::new(),
}
}
})
.collect(),
tags: content.tags.clone(),
language: config.language.clone(),
};
items.push(item);
}
let feed = JsonFeed {
version: "https://jsonfeed.org/version/1".to_string(),
title: config.name.clone(),
home_page_url: config.url.clone(),
feed_url: format!("{}/{}.json", &config.url, filename),
description: config.tagline.clone(),
items,
};
let json = serde_json::to_string_pretty(&feed).map_err(|e| e.to_string())?;
let feed_path = output_path.join(format!("{filename}.json"));
let mut file = File::create(&feed_path).map_err(|e| e.to_string())?;
file.write_all(json.as_bytes()).map_err(|e| e.to_string())?;
info!("Generated {}", &feed_path.display());
Ok(())
}
#[cfg(test)]
#[path = "tests/feed.rs"]
mod tests;