implemented also atom feed via template
This commit is contained in:
		
							parent
							
								
									6e325179a2
								
							
						
					
					
						commit
						66ddfaf94f
					
				
							
								
								
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -108,6 +108,12 @@ version = "1.4.0"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 | 
					checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "base64"
 | 
				
			||||||
 | 
					version = "0.21.7"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "base64"
 | 
					name = "base64"
 | 
				
			||||||
version = "0.22.1"
 | 
					version = "0.22.1"
 | 
				
			||||||
@ -125,6 +131,9 @@ name = "bitflags"
 | 
				
			|||||||
version = "2.6.0"
 | 
					version = "2.6.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 | 
					checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "block-buffer"
 | 
					name = "block-buffer"
 | 
				
			||||||
@ -1104,6 +1113,8 @@ dependencies = [
 | 
				
			|||||||
 "env_logger",
 | 
					 "env_logger",
 | 
				
			||||||
 "feed-rs",
 | 
					 "feed-rs",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 | 
					 "quick-xml",
 | 
				
			||||||
 | 
					 "ron",
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 "slug",
 | 
					 "slug",
 | 
				
			||||||
 "tera",
 | 
					 "tera",
 | 
				
			||||||
@ -1144,6 +1155,7 @@ checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "encoding_rs",
 | 
					 "encoding_rs",
 | 
				
			||||||
 "memchr",
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -1229,6 +1241,18 @@ dependencies = [
 | 
				
			|||||||
 "windows-sys 0.52.0",
 | 
					 "windows-sys 0.52.0",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "ron"
 | 
				
			||||||
 | 
					version = "0.8.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "base64 0.21.7",
 | 
				
			||||||
 | 
					 "bitflags",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_derive",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "rustix"
 | 
					name = "rustix"
 | 
				
			||||||
version = "0.38.43"
 | 
					version = "0.38.43"
 | 
				
			||||||
@ -1667,7 +1691,7 @@ version = "3.0.0-rc5"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "77f9bd1d03fe3bf87c90b115a0e09e22535cbcff3f8d0a6487524f006325f173"
 | 
					checksum = "77f9bd1d03fe3bf87c90b115a0e09e22535cbcff3f8d0a6487524f006325f173"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "base64",
 | 
					 "base64 0.22.1",
 | 
				
			||||||
 "brotli-decompressor",
 | 
					 "brotli-decompressor",
 | 
				
			||||||
 "cc",
 | 
					 "cc",
 | 
				
			||||||
 "cookie_store",
 | 
					 "cookie_store",
 | 
				
			||||||
@ -1695,7 +1719,7 @@ version = "0.2.3"
 | 
				
			|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "a27729fd2c15426f48992a911ce31bd1f29c1cd0898c2c86b410d24f51c99eb3"
 | 
					checksum = "a27729fd2c15426f48992a911ce31bd1f29c1cd0898c2c86b410d24f51c99eb3"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "base64",
 | 
					 "base64 0.22.1",
 | 
				
			||||||
 "http",
 | 
					 "http",
 | 
				
			||||||
 "httparse",
 | 
					 "httparse",
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
 | 
				
			|||||||
@ -9,10 +9,11 @@ clap = { version = "*", features = ["derive"] }
 | 
				
			|||||||
env_logger = "*"
 | 
					env_logger = "*"
 | 
				
			||||||
feed-rs = "*"
 | 
					feed-rs = "*"
 | 
				
			||||||
log = "*"
 | 
					log = "*"
 | 
				
			||||||
 | 
					ron = "*" # todo for development, to check atom-rs internal representation of feeds
 | 
				
			||||||
serde = { version = "*", features = ["derive"] }
 | 
					serde = { version = "*", features = ["derive"] }
 | 
				
			||||||
slug = "*"
 | 
					slug = "*"
 | 
				
			||||||
tera = "*"
 | 
					tera = "*"
 | 
				
			||||||
toml = "*"
 | 
					toml = "*"
 | 
				
			||||||
ureq = { version = "3.0.0-rc5", features = ["brotli", "charset", "gzip", "native-tls"]}
 | 
					ureq = { version = "3.0.0-rc5", features = ["brotli", "charset", "gzip", "native-tls"]}
 | 
				
			||||||
url = "*"
 | 
					url = "*"
 | 
				
			||||||
 | 
					quick-xml = { version = "*", features = ["serialize"] }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										59
									
								
								planet.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								planet.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					p, h1, h2, h3, h4, h5, h6, small {
 | 
				
			||||||
 | 
					  max-width: 48em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
 | 
				
			||||||
 | 
					    color: inherit !important;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ul, ol {
 | 
				
			||||||
 | 
					  /* account for the 1em -webkit-margin-start for the list icon */
 | 
				
			||||||
 | 
					  max-width: 45em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ul,ol,dl, p {
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  margin-top: 1em;
 | 
				
			||||||
 | 
					  margin-bottom: 1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#maincontainer {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    max-width: 80em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#maincontainer main {
 | 
				
			||||||
 | 
					    max-width: 50em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#maincontainer main * {
 | 
				
			||||||
 | 
					    max-width: 50em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#maincontainer aside {
 | 
				
			||||||
 | 
					    margin-left: 5em;
 | 
				
			||||||
 | 
					    max-width: 25em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					article > h2.entry_header {
 | 
				
			||||||
 | 
					    margin-bottom: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.entry_meta {
 | 
				
			||||||
 | 
					    border: 1px thin;
 | 
				
			||||||
 | 
					    padding: 3px 0;
 | 
				
			||||||
 | 
					    background-color: LightBlue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					hr.entry_sep {
 | 
				
			||||||
 | 
					   border: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					hr.entry_sep::before {
 | 
				
			||||||
 | 
					  content: '* * *';
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
use feed_rs::model::Entry;
 | 
					use feed_rs::model::Entry;
 | 
				
			||||||
use feed_rs::model::Feed;
 | 
					use feed_rs::model::Feed;
 | 
				
			||||||
 | 
					use ron::ser::{to_string_pretty, PrettyConfig};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use std::convert::AsRef;
 | 
					use std::convert::AsRef;
 | 
				
			||||||
use std::fs;
 | 
					use std::fs;
 | 
				
			||||||
@ -28,43 +30,47 @@ impl FeedStore {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn slugify_url(url: &Url) -> String {
 | 
					    fn slugify_url(url: &Url) -> String {
 | 
				
			||||||
        let domain = url.domain().unwrap();
 | 
					        let domain = url.domain().unwrap(); // todo don't hide error
 | 
				
			||||||
        let query = url.query().unwrap_or("");
 | 
					        let query = url.query().unwrap_or("");
 | 
				
			||||||
        slug::slugify(format!("{domain}{}{query}", url.path()))
 | 
					        slug::slugify(format!("{domain}{}{query}", url.path()))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn generic_path(&self, url: &Url, ext: &str) -> String {
 | 
				
			||||||
 | 
					        format!("{}/{}{ext}", self.dir.display(), Self::slugify_url(url))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn feed_path(&self, url: &Url) -> String {
 | 
					    fn feed_path(&self, url: &Url) -> String {
 | 
				
			||||||
        format!("{}/{}", self.dir.display(), Self::slugify_url(url))
 | 
					        self.generic_path(url, "")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn fetchdata_path(&self, url: &Url) -> String {
 | 
					    fn fetchdata_path(&self, url: &Url) -> String {
 | 
				
			||||||
        format!("{}.toml", self.feed_path(url))
 | 
					        self.generic_path(url, ".toml")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn load_fetchdata(&self, url: &Url) -> FetchData {
 | 
					    pub fn load_fetchdata(&self, url: &Url) -> Result<FetchData> {
 | 
				
			||||||
        let path = self.fetchdata_path(url);
 | 
					        let path = self.fetchdata_path(url);
 | 
				
			||||||
        if !fs::exists(path.clone()).unwrap() {
 | 
					        if !fs::exists(path.clone())? {
 | 
				
			||||||
            return FetchData::default();
 | 
					            return Ok(FetchData::default());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        toml::from_str(&fs::read_to_string(path).unwrap()).unwrap()
 | 
					        Ok(toml::from_str(&fs::read_to_string(path)?)?)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn has_changed(&self, url: &Url, new_feed: &Feed) -> bool {
 | 
					    fn has_changed(&self, url: &Url, new_feed: &Feed) -> Result<bool> {
 | 
				
			||||||
        let Some(old_feed) = self.load_feed(url, false) else {
 | 
					        let Some(old_feed) = self.load_feed(url, false) else {
 | 
				
			||||||
            return true;
 | 
					            return Ok(true);
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut old_iter = old_feed.entries.iter();
 | 
					        let mut old_iter = old_feed.entries.iter();
 | 
				
			||||||
        for new in &new_feed.entries {
 | 
					        for new in &new_feed.entries {
 | 
				
			||||||
            let Some(old) = old_iter.next() else {
 | 
					            let Some(old) = old_iter.next() else {
 | 
				
			||||||
                return true;
 | 
					                return Ok(true);
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            if old != new {
 | 
					            if old != new {
 | 
				
			||||||
                return true;
 | 
					                return Ok(true);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // ignoring any entries left in old_iter
 | 
					        // ignoring any entries left in old_iter
 | 
				
			||||||
        false
 | 
					        Ok(false)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn write<P: AsRef<std::path::Path> + std::fmt::Display, C: AsRef<[u8]>>(
 | 
					    fn write<P: AsRef<std::path::Path> + std::fmt::Display, C: AsRef<[u8]>>(
 | 
				
			||||||
@ -77,7 +83,7 @@ impl FeedStore {
 | 
				
			|||||||
        fs::write(path, contents)
 | 
					        fs::write(path, contents)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn store(&self, url: &Url, mut response: Response<Body>) -> bool {
 | 
					    pub fn store(&self, url: &Url, mut response: Response<Body>) -> Result<bool> {
 | 
				
			||||||
        let headers = response.headers();
 | 
					        let headers = response.headers();
 | 
				
			||||||
        let fetchdata = FetchData {
 | 
					        let fetchdata = FetchData {
 | 
				
			||||||
            etag: hv(headers, "etag"),
 | 
					            etag: hv(headers, "etag"),
 | 
				
			||||||
@ -94,19 +100,24 @@ impl FeedStore {
 | 
				
			|||||||
            Ok(f) => f,
 | 
					            Ok(f) => f,
 | 
				
			||||||
            Err(e) => {
 | 
					            Err(e) => {
 | 
				
			||||||
                warn!("Error when parsing feed for {url}: {e:?}");
 | 
					                warn!("Error when parsing feed for {url}: {e:?}");
 | 
				
			||||||
                return false;
 | 
					                return Ok(false);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        if !self.has_changed(url, &feed) {
 | 
					        if !self.has_changed(url, &feed)? {
 | 
				
			||||||
            return false;
 | 
					            return Ok(false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        debug!("Storing feed for {url}.");
 | 
					        debug!("Storing feed for {url}.");
 | 
				
			||||||
        let _ = Self::write(self.feed_path(url), body);
 | 
					        // todo don't serialize to string but to writer
 | 
				
			||||||
        let _ = Self::write(
 | 
					        Self::write(
 | 
				
			||||||
 | 
					            self.generic_path(url, ".ron"),
 | 
				
			||||||
 | 
					            to_string_pretty(&feed, PrettyConfig::default())?,
 | 
				
			||||||
 | 
					        )?;
 | 
				
			||||||
 | 
					        Self::write(self.feed_path(url), body)?;
 | 
				
			||||||
 | 
					        Self::write(
 | 
				
			||||||
            self.fetchdata_path(url),
 | 
					            self.fetchdata_path(url),
 | 
				
			||||||
            toml::to_string(&fetchdata).unwrap(),
 | 
					            toml::to_string(&fetchdata).unwrap(),
 | 
				
			||||||
        );
 | 
					        )?;
 | 
				
			||||||
        true
 | 
					        Ok(true)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn load_feed(&self, url: &Url, sanitize: bool) -> Option<Feed> {
 | 
					    fn load_feed(&self, url: &Url, sanitize: bool) -> Option<Feed> {
 | 
				
			||||||
@ -132,6 +143,7 @@ impl FeedStore {
 | 
				
			|||||||
                warn!("Problem parsing feed file for feed {}", feed_config.url);
 | 
					                warn!("Problem parsing feed file for feed {}", feed_config.url);
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            entries.append(&mut feed.entries);
 | 
					            entries.append(&mut feed.entries);
 | 
				
			||||||
            // todo also trim mid-way when length > something, trading cpu for memory
 | 
					            // todo also trim mid-way when length > something, trading cpu for memory
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
use std::time::Instant;
 | 
					use std::time::Instant;
 | 
				
			||||||
use ureq::tls::{TlsConfig, TlsProvider};
 | 
					use ureq::tls::{TlsConfig, TlsProvider};
 | 
				
			||||||
use ureq::Agent;
 | 
					use ureq::Agent;
 | 
				
			||||||
@ -31,8 +32,8 @@ impl Fetcher {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn fetch(&self, url: Url, feed_store: &FeedStore) -> bool {
 | 
					    pub fn fetch(&self, url: Url, feed_store: &FeedStore) -> Result<bool> {
 | 
				
			||||||
        let fetchdata = feed_store.load_fetchdata(&url);
 | 
					        let fetchdata = feed_store.load_fetchdata(&url)?;
 | 
				
			||||||
        let mut builder = self
 | 
					        let mut builder = self
 | 
				
			||||||
            .agent
 | 
					            .agent
 | 
				
			||||||
            .get(url.to_string())
 | 
					            .get(url.to_string())
 | 
				
			||||||
@ -48,7 +49,7 @@ impl Fetcher {
 | 
				
			|||||||
        let result = builder.call();
 | 
					        let result = builder.call();
 | 
				
			||||||
        let duration = start_instant.elapsed();
 | 
					        let duration = start_instant.elapsed();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let response = result.unwrap(); // todo log and return false
 | 
					        let response = result?; // todo log and return false
 | 
				
			||||||
        debug!(
 | 
					        debug!(
 | 
				
			||||||
            "fetched with status {} in {} ms: {url}",
 | 
					            "fetched with status {} in {} ms: {url}",
 | 
				
			||||||
            response.status(),
 | 
					            response.status(),
 | 
				
			||||||
@ -56,14 +57,14 @@ impl Fetcher {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
        let status = response.status();
 | 
					        let status = response.status();
 | 
				
			||||||
        match status.as_u16() {
 | 
					        match status.as_u16() {
 | 
				
			||||||
            304 => false, // Not Modified -> nothing to do
 | 
					            304 => Ok(false), // Not Modified -> nothing to do
 | 
				
			||||||
            200 => feed_store.store(&url, response),
 | 
					            200 => feed_store.store(&url, response),
 | 
				
			||||||
            _ => {
 | 
					            _ => {
 | 
				
			||||||
                warn!(
 | 
					                warn!(
 | 
				
			||||||
                    "HTTP Status {} not implemented for {url}",
 | 
					                    "HTTP Status {} not implemented for {url}",
 | 
				
			||||||
                    response.status()
 | 
					                    response.status()
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                false
 | 
					                Ok(false)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										38
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								src/main.rs
									
									
									
									
									
								
							@ -6,14 +6,14 @@ use crate::fetcher::Fetcher;
 | 
				
			|||||||
use anyhow::Result;
 | 
					use anyhow::Result;
 | 
				
			||||||
use clap::Parser;
 | 
					use clap::Parser;
 | 
				
			||||||
use serde::Deserialize;
 | 
					use serde::Deserialize;
 | 
				
			||||||
use simple_entry::SimpleEntry;
 | 
					 | 
				
			||||||
use std::fs;
 | 
					use std::fs;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
use url::Url;
 | 
					use url::Url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//mod atom_serializer;
 | 
				
			||||||
mod feed_store;
 | 
					mod feed_store;
 | 
				
			||||||
mod fetcher;
 | 
					mod fetcher;
 | 
				
			||||||
mod simple_entry;
 | 
					mod template_engine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Parser)]
 | 
					#[derive(Parser)]
 | 
				
			||||||
#[command(author, version, about, long_about = None)]
 | 
					#[command(author, version, about, long_about = None)]
 | 
				
			||||||
@ -70,42 +70,12 @@ fn fetch(config: &Config, feed_store: &FeedStore) -> Result<bool> {
 | 
				
			|||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        rebuild |= fetcher.fetch(url, feed_store);
 | 
					        rebuild |= fetcher.fetch(url, feed_store)?;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    info!("Done fetching. Rebuild needed: {rebuild}");
 | 
					    info!("Done fetching. Rebuild needed: {rebuild}");
 | 
				
			||||||
    Ok(rebuild)
 | 
					    Ok(rebuild)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn build(config: &Config, feed_store: &FeedStore) -> Result<()> {
 | 
					 | 
				
			||||||
    let templates_dir = to_checked_pathbuf(&config.templates_dir);
 | 
					 | 
				
			||||||
    let out_dir = to_checked_pathbuf(&config.out_dir);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut tera = match tera::Tera::new(&format!("{}/*", &templates_dir.display())) {
 | 
					 | 
				
			||||||
        Ok(t) => t,
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					 | 
				
			||||||
            println!("Parsing error(s): {}", e);
 | 
					 | 
				
			||||||
            ::std::process::exit(1);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    // disable autoescape as this would corrupt urls or the entriy contents. todo check this!
 | 
					 | 
				
			||||||
    tera.autoescape_on(vec![]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut context = tera::Context::new();
 | 
					 | 
				
			||||||
    let entries: Vec<SimpleEntry> = feed_store
 | 
					 | 
				
			||||||
        .collect(&config.feeds)
 | 
					 | 
				
			||||||
        .into_iter()
 | 
					 | 
				
			||||||
        .map(SimpleEntry::from_feed_entry)
 | 
					 | 
				
			||||||
        .collect();
 | 
					 | 
				
			||||||
    context.insert("entries", &entries);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for name in tera.get_template_names() {
 | 
					 | 
				
			||||||
        debug!("Processing template {name}");
 | 
					 | 
				
			||||||
        let file = fs::File::create(format!("{}/{name}", out_dir.display()))?;
 | 
					 | 
				
			||||||
        tera.render_to(name, &context, file)?;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn main() -> Result<()> {
 | 
					fn main() -> Result<()> {
 | 
				
			||||||
    env_logger::init();
 | 
					    env_logger::init();
 | 
				
			||||||
    info!("starting up");
 | 
					    info!("starting up");
 | 
				
			||||||
@ -129,7 +99,7 @@ fn main() -> Result<()> {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if should_build {
 | 
					    if should_build {
 | 
				
			||||||
        build(&config, &feed_store)?;
 | 
					        template_engine::build(&config, &feed_store)?;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,43 +0,0 @@
 | 
				
			|||||||
use feed_rs::model::Entry;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Simplified Feed entry for easier value access in template
 | 
					 | 
				
			||||||
#[derive(serde::Serialize)]
 | 
					 | 
				
			||||||
pub struct SimpleEntry {
 | 
					 | 
				
			||||||
    pub date: String,
 | 
					 | 
				
			||||||
    pub content: String,
 | 
					 | 
				
			||||||
    pub author: String,
 | 
					 | 
				
			||||||
    pub link: String,
 | 
					 | 
				
			||||||
    pub title: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// format for the entries timestamp
 | 
					 | 
				
			||||||
/// <https://docs.rs/chrono/latest/chrono/format/strftime>
 | 
					 | 
				
			||||||
const FMT: &str = "%c";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SimpleEntry {
 | 
					 | 
				
			||||||
    pub fn from_feed_entry(entry: Entry) -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            date: entry
 | 
					 | 
				
			||||||
                .updated
 | 
					 | 
				
			||||||
                .or(entry.published)
 | 
					 | 
				
			||||||
                .unwrap_or_default()
 | 
					 | 
				
			||||||
                .format(FMT)
 | 
					 | 
				
			||||||
                .to_string(),
 | 
					 | 
				
			||||||
            content: entry
 | 
					 | 
				
			||||||
                .content
 | 
					 | 
				
			||||||
                .map(|x| x.body.unwrap_or_default())
 | 
					 | 
				
			||||||
                .unwrap_or_default(),
 | 
					 | 
				
			||||||
            author: if !entry.authors.is_empty() {
 | 
					 | 
				
			||||||
                entry.authors[0].name.clone()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                "".to_string()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            link: if !entry.links.is_empty() {
 | 
					 | 
				
			||||||
                entry.links[0].href.clone()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                "".to_string()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            title: entry.title.map(|x| x.content).unwrap_or_default(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										35
									
								
								src/template_engine.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/template_engine.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					use crate::feed_store::FeedStore;
 | 
				
			||||||
 | 
					use crate::to_checked_pathbuf;
 | 
				
			||||||
 | 
					use crate::Config;
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					use feed_rs::model::Entry;
 | 
				
			||||||
 | 
					use std::fs::File;
 | 
				
			||||||
 | 
					use tera::Tera;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn build(config: &Config, feed_store: &FeedStore) -> Result<()> {
 | 
				
			||||||
 | 
					    let tera = create_tera(&config.templates_dir)?;
 | 
				
			||||||
 | 
					    let out_dir = to_checked_pathbuf(&config.out_dir);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut context = tera::Context::new();
 | 
				
			||||||
 | 
					    let feed_entries: Vec<Entry> = feed_store.collect(&config.feeds);
 | 
				
			||||||
 | 
					    context.insert("entries", &feed_entries);
 | 
				
			||||||
 | 
					    context.insert("PKG_AUTHORS", env!("CARGO_PKG_AUTHORS"));
 | 
				
			||||||
 | 
					    context.insert("PKG_HOMEPAGE", env!("CARGO_PKG_HOMEPAGE"));
 | 
				
			||||||
 | 
					    context.insert("PKG_NAME", env!("CARGO_PKG_NAME"));
 | 
				
			||||||
 | 
					    context.insert("PKG_VERSION", env!("CARGO_PKG_VERSION"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for name in tera.get_template_names() {
 | 
				
			||||||
 | 
					        debug!("Processing template {name}");
 | 
				
			||||||
 | 
					        let file = File::create(format!("{}/{name}", out_dir.display()))?;
 | 
				
			||||||
 | 
					        tera.render_to(name, &context, file)?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn create_tera(templates_dir: &str) -> Result<Tera> {
 | 
				
			||||||
 | 
					    let dir = to_checked_pathbuf(templates_dir);
 | 
				
			||||||
 | 
					    let mut tera = tera::Tera::new(&format!("{}/*", &dir.display()))?;
 | 
				
			||||||
 | 
					    // disable autoescape as this would corrupt urls or the entriy contents. todo check this!
 | 
				
			||||||
 | 
					    tera.autoescape_on(vec![]);
 | 
				
			||||||
 | 
					    Ok(tera)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										52
									
								
								templates/atom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								templates/atom.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<feed xmlns="http://www.w3.org/2005/Atom">
 | 
				
			||||||
 | 
					  <title>Planet TVL</title>
 | 
				
			||||||
 | 
					  <link href="https::/planet.tvl.fyi"/>
 | 
				
			||||||
 | 
					  <updated>{{now()|date(format="%Y-%m-%dT%H:%M:%SZ")}}</updated>
 | 
				
			||||||
 | 
					  <id>https::/planet.tvl.fyi</id>
 | 
				
			||||||
 | 
					  <generator uri="{{ PKG_HOMEPAGE }}" version="{{ PKG_VERSION }}">
 | 
				
			||||||
 | 
					    {{ PKG_NAME }} by {{ PKG_AUTHORS }}
 | 
				
			||||||
 | 
					  </generator>
 | 
				
			||||||
 | 
					  <icon>https://planet.tvl.fyi/logo.svg</icon>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  {% for entry in entries %}
 | 
				
			||||||
 | 
					    <entry>
 | 
				
			||||||
 | 
					      <id>{{ entry.id }}/planet.tvl.fyi</id>
 | 
				
			||||||
 | 
					      {% if entry.title -%}
 | 
				
			||||||
 | 
					        <title>{{ entry.title.content }}</title>
 | 
				
			||||||
 | 
					      {% endif -%}
 | 
				
			||||||
 | 
					      {% for link in entry.links %}
 | 
				
			||||||
 | 
					        <link href="{{ link.href }}" {% if link.rel %}rel="{{ link.rel }}"{% endif %}/>
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					      {% if entry.updated %}
 | 
				
			||||||
 | 
					        <updated>{{ entry.updated }}</updated>
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      {% if entry.published %}
 | 
				
			||||||
 | 
					        <published>{{ entry.published }}</published>
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      {% if entry.summary -%}
 | 
				
			||||||
 | 
					        <summary>
 | 
				
			||||||
 | 
					          {{ entry.summary.content|escape }}
 | 
				
			||||||
 | 
					        </summary>
 | 
				
			||||||
 | 
					      {% endif -%}
 | 
				
			||||||
 | 
					      {% for author in entry.authors %}
 | 
				
			||||||
 | 
					        <author>
 | 
				
			||||||
 | 
					          {% if author.name -%}
 | 
				
			||||||
 | 
					            <name>{{ author.name }}</name>
 | 
				
			||||||
 | 
					          {% endif -%}
 | 
				
			||||||
 | 
					          {% if author.email -%}
 | 
				
			||||||
 | 
					            <email>{{ author.email }}</email>
 | 
				
			||||||
 | 
					          {% endif -%}
 | 
				
			||||||
 | 
					        </author>
 | 
				
			||||||
 | 
					        {% if author.email -%}
 | 
				
			||||||
 | 
					          <uri>{{ author.uri }}</uri>
 | 
				
			||||||
 | 
					        {% endif -%}
 | 
				
			||||||
 | 
					      {% endfor %}
 | 
				
			||||||
 | 
					      {% if entry.content -%}
 | 
				
			||||||
 | 
					        <content {% if entry.content.type %}type="{{ entry.content.type }}"{% endif %} {% if entry.content.src %}type="{{ entry.content.src }}"{% endif %}>
 | 
				
			||||||
 | 
					          {{ entry.content.body|escape }}
 | 
				
			||||||
 | 
					        </content>
 | 
				
			||||||
 | 
					      {% endif -%}
 | 
				
			||||||
 | 
					    </entry>
 | 
				
			||||||
 | 
					  {% endfor %}
 | 
				
			||||||
 | 
					</feed>
 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					{% set dateformat = "%d.%m.%Y %H:%M" -%}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<html>
 | 
					<html>
 | 
				
			||||||
  <head>
 | 
					  <head>
 | 
				
			||||||
    <title>Planet TVL</title>
 | 
					    <title>Planet TVL</title>
 | 
				
			||||||
@ -5,7 +7,6 @@
 | 
				
			|||||||
    <meta name="generator" content="planet-mars">
 | 
					    <meta name="generator" content="planet-mars">
 | 
				
			||||||
    <link rel="shortcut icon" href="/favicon.ico">
 | 
					    <link rel="shortcut icon" href="/favicon.ico">
 | 
				
			||||||
    <link rel="stylesheet" href="planet.css" type="text/css">
 | 
					    <link rel="stylesheet" href="planet.css" type="text/css">
 | 
				
			||||||
    {# todo <link rel="alternate" type="application/rss+xml" title="Planet Haskell RSS Feed" href="rss20.xml"> #}
 | 
					 | 
				
			||||||
    <link rel="alternate" type="application/xml+atom" title="Planet Haskell Atom Feed" href="atom.xml">
 | 
					    <link rel="alternate" type="application/xml+atom" title="Planet Haskell Atom Feed" href="atom.xml">
 | 
				
			||||||
  </head>
 | 
					  </head>
 | 
				
			||||||
  <body>
 | 
					  <body>
 | 
				
			||||||
@ -14,17 +15,50 @@
 | 
				
			|||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
    <div id="maincontainer">
 | 
					    <div id="maincontainer">
 | 
				
			||||||
      <main>
 | 
					      <main>
 | 
				
			||||||
        {% for entry in entries %}
 | 
					        {% for entry in entries -%}
 | 
				
			||||||
 | 
					          {% if loop.index > 1 -%}
 | 
				
			||||||
 | 
					            <hr class="entry_sep">
 | 
				
			||||||
 | 
					          {% endif -%}
 | 
				
			||||||
 | 
					          {% if entry.links.0 -%}
 | 
				
			||||||
 | 
					            {% set link = entry.links.0.href -%}
 | 
				
			||||||
 | 
					          {% else -%}
 | 
				
			||||||
 | 
					            {% set link = "" -%}
 | 
				
			||||||
 | 
					          {% endif -%}
 | 
				
			||||||
          <article>
 | 
					          <article>
 | 
				
			||||||
            <h2><a href="{{entry.link}}">{{ entry.title|striptags }}<a></h2>
 | 
					            <h2 class="entry_header">
 | 
				
			||||||
            <date>{% if entry.published %}{{ entry.published | date(format="%Y-%m-%d %H:%M", timezone="Europe/Moscow") }}{% endif %}</date>{# todo: maybe group posts by day? #}
 | 
					              <a {% if link -%}href="{{link}}"{% endif -%}>
 | 
				
			||||||
            <p class="entry_author">{{ entry.author|striptags }}</p>
 | 
					                {% if entry.title -%}
 | 
				
			||||||
            <div class="entry_content">
 | 
					                  {{ entry.title.content|striptags }}
 | 
				
			||||||
              {{ entry.content }}
 | 
					                {% else -%}
 | 
				
			||||||
 | 
					                  NO TITLE
 | 
				
			||||||
 | 
					                {% endif -%}
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					            </h2>
 | 
				
			||||||
 | 
					            <div class="entry_meta">
 | 
				
			||||||
 | 
					              <date>
 | 
				
			||||||
 | 
					                {% if entry.updated -%}
 | 
				
			||||||
 | 
					                  <span>{{ entry.updated | date(format=dateformat) }}</span>
 | 
				
			||||||
 | 
					                {% else -%}
 | 
				
			||||||
 | 
					                  <span>{{ entry.published | date(format=dateformat) }}</span>
 | 
				
			||||||
 | 
					                {% endif -%}
 | 
				
			||||||
 | 
					              </date>
 | 
				
			||||||
 | 
					              {% if entry.authors -%}
 | 
				
			||||||
 | 
					                — <span class="entry_author">{{ entry.authors.0.name | striptags }}</span>
 | 
				
			||||||
 | 
					              {% endif -%}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <p><a href="{{entry.link}}">full post</a></p>
 | 
					
 | 
				
			||||||
 | 
					            {% if entry.summary -%}
 | 
				
			||||||
 | 
					              <div class="entry_summary">
 | 
				
			||||||
 | 
					                {{ entry.summary.content }}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {% endif -%}
 | 
				
			||||||
 | 
					            {% if entry.content -%}
 | 
				
			||||||
 | 
					              <div class="entry_content">
 | 
				
			||||||
 | 
					                {{ entry.content.body }}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {% endif -%}
 | 
				
			||||||
          </article>
 | 
					          </article>
 | 
				
			||||||
        {% endfor %}
 | 
					        {% endfor -%}
 | 
				
			||||||
      </main>
 | 
					      </main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <aside>
 | 
					      <aside>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user