bulletty 0.2.1

a pretty TUI feed reader (RSS+ATOM) that stores articles locally as Markdown files
use color_eyre::{Result, eyre};
use roxmltree::Node;

use crate::core::library::feedcategory::FeedCategory;

pub struct OpmlFeed {
    pub url: String,
    pub category: Option<String>,
}

pub fn get_opml_feeds(filename: &str) -> Result<Vec<OpmlFeed>> {
    let doc = std::fs::read_to_string(filename)?;
    let doc = roxmltree::Document::parse(&doc)?;

    let body = doc.descendants().find(|n| n.has_tag_name("body"));
    if body.is_none() {
        return Err(eyre::eyre!("No body found in {:?}", filename));
    }

    let outlines: Vec<Node> = body
        .unwrap()
        .children()
        .filter(|n| n.is_element() && n.has_tag_name("outline"))
        .collect();

    let mut opml_feeds = Vec::<OpmlFeed>::new();

    for o in outlines.iter() {
        if o.has_attribute("xmlUrl") {
            if let Ok(feed) = get_opml_feed(o, None) {
                opml_feeds.push(feed);
            }
        } else {
            let title = o.attribute("title").unwrap_or("");
            let feeds: Vec<OpmlFeed> = o
                .children()
                .map(|c| get_opml_feed(&c, Some(title.to_string())))
                .filter_map(Result::ok)
                .collect();

            opml_feeds.extend(feeds);
        }
    }

    Ok(opml_feeds)
}

fn get_opml_feed(node: &Node, category: Option<String>) -> Result<OpmlFeed> {
    if let Some(xml_url) = node.attribute("xmlUrl") {
        Ok(OpmlFeed {
            url: xml_url.to_string(),
            category,
        })
    } else {
        Err(eyre::eyre!("No xml attribute found in element"))
    }
}

pub fn save_opml(categories: &[FeedCategory], filename: &str) -> Result<()> {
    let mut text_categories = String::new();
    for category in categories.iter() {
        let mut text_feeds = String::new();
        for feed in category.feeds.iter() {
            let title = html_escape::encode_text(&feed.title);
            let description = html_escape::encode_text(&feed.description);

            text_feeds.push_str(&format!("\n            <outline text={:?} title={:?} description={:?} xmlUrl={:?} type=\"rss\" />", title, title, description, feed.feed_url));
        }

        let title = html_escape::encode_text(&category.title);

        text_categories.push_str(&format!(
            "\n        <outline text={:?} title={:?}>{}\n        </outline>",
            title, title, text_feeds
        ));
    }

    let opml = format!(
        r#"<?xml version='1.0' encoding='UTF-8' ?>
<opml version="1.0">
    <head>
        <title>Generated from bulletty</title>
        <url>https://github.com/CrociDB/bulletty</url>
    </head>
    <body>{text_categories}
    </body>
</opml>
"#
    );

    std::fs::write(filename, opml)?;

    Ok(())
}