kyanite 0.5.1

A modular, expandable, gallery collector.
use crate::collector::KyaniteCollector;
use crate::item::KyaniteItem;
use log::{debug, info};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Default)]
pub struct E621Collector;

impl E621Collector {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn boxed() -> Box<dyn KyaniteCollector> {
        Box::new(Self::new())
    }
}

#[async_trait::async_trait]
impl KyaniteCollector for E621Collector {
    fn id(&self) -> &'static str {
        "e621"
    }

    fn name(&self) -> &'static str {
        "E621"
    }

    fn api_base(&self) -> &'static str {
        "https://e621.net/posts.json"
    }

    fn site_base(&self) -> &'static str {
        "https://e621.net"
    }

    fn tags_argument(&self) -> &'static str {
        "tags"
    }

    fn page_argument(&self) -> &'static str {
        "page"
    }

    async fn collect(&self, tags: Vec<String>) -> anyhow::Result<Vec<KyaniteItem>> {
        info!("Starting {} collector...", &self.name());
        let mut items = Vec::new();
        let mut page = 1u64; // Starts at 1.
        let mut finished = false;
        while !finished {
            debug!("Grabbing page with Reqwest GET...");
            let joined_tags = tags.clone().join("+");
            let resp = reqwest::get(&self.api_by_page(joined_tags, page)).await?;
            debug!("Reading the page body as text...");
            let body = resp.text().await?;
            debug!("Deserializing posts...");
            let posts: Vec<E621Post> = match serde_json::from_str::<E621Response>(&body) {
                Ok(resp) => resp.posts,
                Err(why) => {
                    debug!(
                        "Failed getting page {} of {}, gracefully ending collection: {}",
                        page,
                        self.name(),
                        why
                    );
                    Vec::new()
                }
            };
            info!(
                "Found {} {} on page {} of {}...",
                posts.len(),
                if posts.len() == 1 { "post" } else { "posts" },
                page,
                self.name()
            );
            if posts.is_empty() {
                finished = true;
                info!("Page {} is empty, stopping collection.", &page);
            } else {
                for post in posts {
                    let url = post.file.url.unwrap_or_else(|| "".to_owned());
                    if !url.is_empty() {
                        items.push(KyaniteItem::new(url, tags.clone(), self.id().to_owned()));
                    }
                }
                page += 1;
            }
        }
        Ok(items)
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct E621Response {
    pub posts: Vec<E621Post>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct E621Post {
    pub file: E621PostFile,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct E621PostFile {
    pub md5: String,
    pub url: Option<String>,
}