diff --git a/Cargo.toml b/Cargo.toml
index 1e0cb0cc..66fc7c2b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,7 +46,7 @@ fastrand = "2.0.1"
log = "0.4.20"
pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
-rss = "2.0.7"
+rss = "2.0.12"
arc-swap = "1.7.1"
serde_json_path = "0.7.1"
async-recursion = "1.1.1"
diff --git a/src/subreddit.rs b/src/subreddit.rs
index 4046be1c..172269b9 100644
--- a/src/subreddit.rs
+++ b/src/subreddit.rs
@@ -3,8 +3,7 @@
use crate::{config, utils};
// CRATES
use crate::utils::{
- catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences,
- Subreddit,
+ Post, Preferences, Subreddit, catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, to_absolute_url, val
};
use crate::{client::json, server::RequestExt, server::ResponseExt};
use askama::Template;
@@ -14,6 +13,7 @@ use hyper::{Body, Request, Response};
use chrono::DateTime;
use regex::Regex;
+use rss::{ChannelBuilder, Item, Enclosure};
use std::sync::LazyLock;
use time::{Duration, OffsetDateTime};
@@ -595,7 +595,6 @@ pub async fn rss(req: Request
) -> Result, String> {
}
use hyper::header::CONTENT_TYPE;
- use rss::{ChannelBuilder, Item};
// Get subreddit
let sub = req.param("sub").unwrap_or_default();
@@ -605,6 +604,9 @@ pub async fn rss(req: Request) -> Result, String> {
// Get path
let path = format!("/r/{sub}/{sort}.json?{}", req.uri().query().unwrap_or_default());
+ // Get subreddit link
+ let subreddit_link: String = format!("{}/r/{sub}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default());
+
// Get subreddit data
let subreddit = subreddit(&sub, false).await?;
@@ -615,21 +617,23 @@ pub async fn rss(req: Request) -> Result, String> {
let channel = ChannelBuilder::default()
.title(&subreddit.title)
.description(&subreddit.description)
+ .link(&subreddit_link)
.items(
posts
.into_iter()
- .map(|post| Item {
- title: Some(post.title.to_string()),
- link: Some(format_url(&utils::get_post_url(&post))),
- author: Some(post.author.name),
- content: Some(rewrite_urls(&decode_html(&post.body).unwrap())),
- pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()),
- description: Some(format!(
- "Comments",
- config::get_setting("REDLIB_FULL_URL").unwrap_or_default(),
- post.permalink
- )),
- ..Default::default()
+ .map(|post| {
+ let mut item = Item {
+ title: Some(post.title.to_string()),
+ link: Some(format_url(&utils::get_post_url(&post))),
+ author: Some(post.author.name.to_string()),
+ content: Some(rewrite_urls(&decode_html(&post.body).unwrap())),
+ pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()),
+ description: Some(format!("Comments", to_absolute_url(&post.permalink))),
+ ..Default::default()
+ };
+
+ apply_enclosure(&mut item, &post);
+ item
})
.collect::>(),
)
@@ -645,6 +649,73 @@ pub async fn rss(req: Request) -> Result, String> {
Ok(res)
}
+// Set enclosure image for RSS feed item
+fn apply_enclosure(item: &mut Item, post: &Post) {
+ item.set_enclosure(get_rss_image(&post));
+
+ // Embed the number of gallery images in description and content since
+ // only the first image in the gallery is used for the enclosure
+ if post.post_type == "gallery" && post.gallery.len() > 1 {
+ item.set_description(
+ format!("Gallery with {} images",
+ to_absolute_url(&post.permalink),
+ post.gallery.len()
+ )
+ );
+
+ if let Some(content) = item.content() {
+ let new_content = format!(
+ "{}
{}",
+ item.description().unwrap_or(""),
+ content,
+ );
+ item.set_content(new_content);
+ }
+ }
+
+}
+
+fn get_rss_image(post: &Post) -> Option {
+ let image_url = match post.post_type.as_str() {
+ "image" => Some(post.media.url.clone()),
+ "gallery" => post.gallery.get(0).and_then(|media| decode_html(&media.url).ok()),
+ "gif" | "video" => decode_html(&post.media.poster).ok(),
+ _ => None,
+ };
+
+ image_url.map(|url| {
+ let mut enclosure = Enclosure::default();
+ enclosure.set_mime_type(get_mime_type(&url));
+ enclosure.set_url(to_absolute_url(&url));
+ enclosure.set_length("0");
+ enclosure
+ })
+}
+
+/// Determines the MIME type based on file extension in a URL.
+/// Handles both absolute and relative URLs with query parameters.
+fn get_mime_type(url: &str) -> &'static str {
+ // Extract the path component, removing query parameters
+ let path = url.split('?').next().unwrap_or(url);
+
+ // Get the file extension (everything after the last dot)
+ let extension = path
+ .rsplit('.')
+ .next()
+ .unwrap_or("")
+ .to_lowercase();
+
+ // Match common image extensions
+ match extension.as_str() {
+ "jpg" | "jpeg" => "image/jpeg",
+ "png" => "image/png",
+ "gif" => "image/gif",
+ "webp" => "image/webp",
+ "svg" => "image/svg+xml",
+ _ => "application/octet-stream",
+ }
+}
+
#[tokio::test(flavor = "multi_thread")]
async fn test_fetching_subreddit() {
let subreddit = subreddit("rust", false).await;
diff --git a/src/utils.rs b/src/utils.rs
index efe98b7d..d323c711 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1433,18 +1433,23 @@ pub fn url_path_basename(path: &str) -> String {
}
}
-/// Returns the URL of a post, as needed by RSS feeds
+/// Returns the absolute URL of a post, as needed by RSS feeds
pub fn get_post_url(post: &Post) -> String {
+ match post.post_type.as_str() {
+ "image" | "gallery" | "gif" | "video" => return to_absolute_url(&post.permalink),
+ _ => {}
+ }
+
if let Some(out_url) = &post.out_url {
- // Handle cross post
- if out_url.starts_with("/r/") {
- format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), out_url)
- } else {
- out_url.to_string()
- }
- } else {
- format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), post.permalink)
+ return if out_url.starts_with("/r/") { to_absolute_url(out_url) } else { out_url.clone() };
}
+
+ to_absolute_url(&post.permalink)
+}
+
+/// Returns an absolute URL given a relative URL, as needed by RSS feeds
+pub fn to_absolute_url(relative_path: &str) -> String {
+ format!("{}{}", config::get_setting("REDLIB_FULL_URL").unwrap_or_default(), relative_path)
}
#[cfg(test)]