Skip to content

Commit 9264388

Browse files
committed
feat: adding feed and fixing website module
1 parent dbcfe49 commit 9264388

8 files changed

Lines changed: 102 additions & 13 deletions

File tree

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
mod website;
1+
mod site;
22

33
use std::path::Path;
44

55
use clap::{Parser, Subcommand};
6-
use website::{MediaLibrary, Website};
6+
use site::{MediaLibrary, Website};
77

88
#[derive(Parser)]
99
#[command(author, version, about)]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ mod website_info;
44
mod website_item;
55

66
pub use media_library::MediaLibrary;
7-
pub use website::{BuildReport, Website};
7+
pub use website::Website;
Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ use std::time::{Duration, Instant};
66
use rayon::prelude::*;
77

88
use super::media_library::{MediaLibrary, MediaLibraryError};
9-
use super::website_info::{SITE_TOML, WebsiteInfo, WebsiteInfoError};
9+
use super::website_info::{WebsiteInfo, WebsiteInfoError, SITE_TOML};
1010
use super::website_item::{GenerationResult, WebsiteItem};
1111

1212
const DEFAULT_BUILD_DIR: &str = "build";
1313
const DEFAULT_MEDIA_DIR: &str = "media";
1414
const DEFAULT_PUBLIC_DIR: &str = "public";
1515

16+
const DEFAULT_FEED_FILE: &str = "feed.xml";
17+
1618
const TEMPLATE_INDEX: &str = include_str!("../../template/index.html");
1719
const TEMPLATE_STYLE: &str = include_str!("../../template/public/style.css");
1820
const TEMPLATE_JS: &str = include_str!("../../template/public/clutterlog.js");
1921
const TEMPLATE_GITHUB_ACTION: &str = include_str!("../../template/github_action.yaml");
22+
const TEMPLATE_RSS: &str = include_str!("../../template/rss.xml");
2023

2124
pub struct BuildReport {
2225
pub items_processed: usize,
@@ -171,7 +174,7 @@ impl Website {
171174
library.update_metadata(&source_media_path)?;
172175

173176
// Scan source media directory, copy files, generate thumbnails, and collect data entries
174-
let (clutterlog_data, generation_results) =
177+
let (clutterlog_data, rss_items, generation_results) =
175178
self.scan_and_copy_media(&source_media_path, &build_media_path, &library)?;
176179

177180
// Render index.html from template
@@ -184,6 +187,23 @@ impl Website {
184187
let index_path = build_path.join("index.html");
185188
fs::write(&index_path, &rendered).map_err(|e| WebsiteError::Io(index_path, e))?;
186189

190+
// Render and write feed.xml
191+
let rss_items_str = if rss_items.is_empty() {
192+
String::new()
193+
} else {
194+
format!("{}\n", rss_items.join("\n"))
195+
};
196+
let base_url = self.info.url.trim_end_matches('/');
197+
let feed_url = format!("{}/", base_url);
198+
let rss_rendered = TEMPLATE_RSS
199+
.replace("{{title}}", &escape_html(&self.info.title))
200+
.replace("{{url}}", &escape_html(&feed_url))
201+
.replace("{{description}}", &escape_html(&self.info.description))
202+
.replace("{{items}}", &rss_items_str);
203+
204+
let rss_path = build_path.join(DEFAULT_FEED_FILE);
205+
fs::write(&rss_path, &rss_rendered).map_err(|e| WebsiteError::Io(rss_path, e))?;
206+
187207
// Write static assets
188208
let style_path = public_path.join("style.css");
189209
fs::write(&style_path, TEMPLATE_STYLE).map_err(|e| WebsiteError::Io(style_path, e))?;
@@ -202,11 +222,11 @@ impl Website {
202222
source_path: &Path,
203223
dest_path: &Path,
204224
library: &MediaLibrary,
205-
) -> Result<(String, Vec<GenerationResult>), WebsiteError> {
225+
) -> Result<(String, Vec<String>, Vec<GenerationResult>), WebsiteError> {
206226
let base_url = self.info.url.trim_end_matches('/');
207227

208228
if !source_path.exists() {
209-
return Ok(("[]".to_string(), Vec::new()));
229+
return Ok(("[]".to_string(), Vec::new(), Vec::new()));
210230
}
211231

212232
let dir_entries = fs::read_dir(source_path)
@@ -224,23 +244,30 @@ impl Website {
224244
.collect();
225245

226246
// Process items in parallel: copy files and generate thumbnails
227-
let processed: Vec<Result<(GenerationResult, String), WebsiteError>> = items
247+
let processed: Vec<Result<(GenerationResult, String, String), WebsiteError>> = items
228248
.par_iter()
229249
.filter_map(|(path, datetime)| {
230250
let item = WebsiteItem::from_path(path, datetime.as_deref())?;
231251
let result = item.copy_and_generate_thumb(dest_path);
232252
let entry = item.to_json_entry(base_url, DEFAULT_MEDIA_DIR);
233-
Some(result.map(|r| (r, entry)))
253+
let rss_item = item.to_rss_item(base_url, DEFAULT_MEDIA_DIR);
254+
let image_url = item.image_url(base_url, DEFAULT_MEDIA_DIR);
255+
Some(result.map(|mut r| {
256+
r.image_url = image_url;
257+
(r, entry, rss_item)
258+
}))
234259
})
235260
.collect();
236261

237262
// Collect results, propagating any errors
238263
let mut results: Vec<GenerationResult> = Vec::new();
239264
let mut entries: Vec<String> = Vec::new();
265+
let mut rss_items: Vec<String> = Vec::new();
240266
for item_result in processed {
241-
let (gen_result, entry) = item_result?;
267+
let (gen_result, entry, rss_item) = item_result?;
242268
results.push(gen_result);
243269
entries.push(entry);
270+
rss_items.push(rss_item);
244271
}
245272

246273
let json = if entries.is_empty() {
@@ -249,7 +276,7 @@ impl Website {
249276
format!("[\n{}\n ]", entries.join(",\n"))
250277
};
251278

252-
Ok((json, results))
279+
Ok((json, rss_items, results))
253280
}
254281
}
255282

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ pub const SUPPORTED_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif",
1515
const THUMB_SIZE: u32 = 350;
1616

1717
pub struct GenerationResult {
18-
pub filename: String,
1918
pub media_size: u64,
2019
pub thumb_size: u64,
20+
pub image_url: String,
2121
}
2222

2323
pub struct WebsiteItem {
@@ -106,9 +106,9 @@ impl WebsiteItem {
106106
.len();
107107

108108
Ok(GenerationResult {
109-
filename: self.filename.clone(),
110109
media_size,
111110
thumb_size,
111+
image_url: String::new(), // filled in by scan_and_copy_media
112112
})
113113
}
114114

@@ -195,6 +195,58 @@ impl WebsiteItem {
195195
escape_js(&self.datetime),
196196
)
197197
}
198+
199+
pub fn image_url(&self, base_url: &str, media_dir: &str) -> String {
200+
format!("{}/{}/{}", base_url, media_dir, self.filename)
201+
}
202+
203+
pub fn to_rss_item(&self, base_url: &str, media_dir: &str) -> String {
204+
let base_url = base_url.trim_end_matches('/');
205+
let image_url = self.image_url(base_url, media_dir);
206+
let item_link = format!("{}/#media={}", base_url, self.filename);
207+
let pub_date = datetime_to_rfc2822(&self.datetime);
208+
let mime = mime_type(&self.extension);
209+
210+
format!(
211+
" <item>\n <title>{}</title>\n <link>{}</link>\n <guid>{}</guid>\n <pubDate>{}</pubDate>\n <enclosure url=\"{}\" type=\"{}\" length=\"0\"/>\n </item>",
212+
escape_xml(&self.title),
213+
escape_xml(&item_link),
214+
escape_xml(&image_url),
215+
pub_date,
216+
escape_xml(&image_url),
217+
mime,
218+
)
219+
}
220+
}
221+
222+
fn mime_type(extension: &str) -> &'static str {
223+
match extension {
224+
"jpg" | "jpeg" => "image/jpeg",
225+
"png" => "image/png",
226+
"webp" => "image/webp",
227+
"gif" => "image/gif",
228+
"webm" => "video/webm",
229+
"mp4" => "video/mp4",
230+
_ => "application/octet-stream",
231+
}
232+
}
233+
234+
fn datetime_to_rfc2822(datetime: &str) -> String {
235+
use chrono::NaiveDateTime;
236+
NaiveDateTime::parse_from_str(datetime, "%Y-%m-%dT%H:%M:%S")
237+
.map(|ndt| {
238+
let dt = ndt.and_utc();
239+
dt.format("%a, %d %b %Y %H:%M:%S +0000").to_string()
240+
})
241+
.unwrap_or_else(|_| datetime.to_string())
242+
}
243+
244+
fn escape_xml(s: &str) -> String {
245+
s.replace('&', "&amp;")
246+
.replace('<', "&lt;")
247+
.replace('>', "&gt;")
248+
.replace('"', "&quot;")
249+
.replace('\'', "&apos;")
198250
}
199251

200252
fn center_crop_resize(img: &image::DynamicImage, size: u32) -> image::DynamicImage {

template/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>{{title}}</title>
77
<link rel="stylesheet" href="public/style.css">
8+
<link rel="alternate" type="application/rss+xml" title="{{title}}" href="feed.xml">
89
</head>
910
<body>
1011
<main>

template/rss.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
3+
<channel>
4+
<title>{{title}}</title>
5+
<link>{{url}}</link>
6+
<description>{{description}}</description>
7+
<atom:link href="{{url}}feed.xml" rel="self" type="application/rss+xml"/>
8+
{{items}} </channel>
9+
</rss>

0 commit comments

Comments
 (0)