root/src/booru/zero.rs

// zero.rs
//
// Copyright 2020 nee <nee-git@hidamari.blue>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
#![allow(clippy::vec_box)]
use crate::booru::Booru;
use crate::booru::Boorus;
use crate::booru::Post;
use crate::booru::Posts;
use crate::booru::http::http_client_zerobooru;
use anyhow::Result;
use serde::Deserialize;

#[derive(Deserialize, Debug, Clone)]
struct Wrapper {
    items: Vec<Box<ZeroIndexPost>>,
}

#[allow(dead_code)]
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ZeroIndexPost {
    id: u64,
    tag: String, // main tag (usually character)
    tags: Vec<String>,
    thumbnail: String,
    source: String,
    width: u32,
    height: u32,
    #[serde(skip_deserializing)]
    domain: String,
}

// Zero booru requires 2 requests in order to obtain the full image url
#[allow(dead_code)]
#[derive(Deserialize, Debug, Clone)]
pub struct ZeroDetailPost {
    id: u64,
    // urls
    small: String,
    medium: String,
    large: String,
    full: String,

    // source: String,
    width: u32,
    height: u32,
    size: u32,
    hash: String,
    primary: String, // main tag (usually character)
    tags: Vec<String>,
    // TODO not written
    #[serde(skip_deserializing)]
    domain: String,
}

impl Post for ZeroIndexPost {
    fn id(&self) -> u64 {
        self.id
    }
    fn sort_id(&self) -> u64 {
        self.id
    }
    fn width(&self) -> u32 {
        self.width
    }
    fn height(&self) -> u32 {
        self.height
    }
    fn sample_url(&self) -> String {
        self.thumbnail.to_owned()
    }
    fn thumb_url(&self) -> String {
        self.thumbnail.to_owned()
    }
    fn full_url(&self) -> String {
        "".to_owned()
    }
    fn filename(&self) -> String {
        let dot_pos = self
            .thumb_url()
            .rfind('.')
            .unwrap_or(self.thumb_url().len());
        let ext = self.thumb_url()[dot_pos..].to_owned();
        format!("zero-{}{}", &self.id, &ext)
    }
    fn tags(&self) -> Vec<&str> {
        self.tags.iter().map(|s| s.as_ref()).collect()
    }
    fn web_url(&self) -> String {
        format!("{}/{}", self.domain(), self.id())
    }
    fn domain(&self) -> &String {
        &self.domain
    }
    fn clone_post(&self) -> Box<dyn Post> {
        std::boxed::Box::new(self.clone())
    }
    fn imp(&self) -> Posts {
        Posts::ZerobooruIndex(self.clone())
    }
}

impl ZeroIndexPost {
    pub async fn fetch_full(&self) -> Result<(bytes::Bytes, Option<std::boxed::Box<dyn Post>>)> {
        // TODO use domain from booru config
        let url = format!("https://{}{}{}{}", &self.domain, "/", self.id(), "?json");
        println!("ZERO {}", url);
        let request = http_client_zerobooru().get(&url).send().await?;
        let mut detail_post = request.json::<Box<ZeroDetailPost>>().await?;

        detail_post.tags = detail_post
            .tags
            .into_iter()
            .map(|t| standard_tag_format(&t))
            .collect();
        let (bytes, _) = detail_post.fetch_full().await?;
        Ok((bytes, Some(Box::<dyn Post>::from(detail_post))))
    }
}

impl Post for ZeroDetailPost {
    fn id(&self) -> u64 {
        self.id
    }
    fn sort_id(&self) -> u64 {
        self.id
    }
    fn width(&self) -> u32 {
        self.width
    }
    fn height(&self) -> u32 {
        self.height
    }
    fn sample_url(&self) -> String {
        self.medium.to_owned()
    }
    fn thumb_url(&self) -> String {
        self.small.to_owned()
    }
    fn full_url(&self) -> String {
        self.full.to_owned()
    }
    fn filename(&self) -> String {
        let dot_pos = self.full_url().rfind('.').unwrap_or(self.full_url().len());
        let ext = self.full_url()[dot_pos..].to_owned();
        ["", &self.hash, &ext].join("")
    }
    fn tags(&self) -> Vec<&str> {
        self.tags.iter().map(|s| s.as_ref()).collect()
    }
    fn web_url(&self) -> String {
        format!("https://{}/{}", self.domain(), self.id())
    }
    fn domain(&self) -> &String {
        &self.domain
    }
    fn clone_post(&self) -> Box<dyn Post> {
        std::boxed::Box::new(self.clone())
    }
    fn imp(&self) -> Posts {
        Posts::ZerobooruDetail(self.clone())
    }
}

impl ZeroDetailPost {
    pub async fn fetch_full(&self) -> Result<(bytes::Bytes, Option<std::boxed::Box<dyn Post>>)> {
        println!("fetching the img {}", &self.full_url());
        let url = self.full_url();

        let request = http_client_zerobooru().get(&url).send().await;
        let binary = request?.bytes().await?;
        Ok((binary, None))
    }
}

fn standard_tag_format(s: &str) -> String {
    s.replace(' ', "_").to_lowercase()
}

#[derive(Deserialize, Debug, Clone)]
pub struct Zerobooru {
    domain: String,
}
impl Booru for Zerobooru {
    fn clone_booru(&self) -> Box<dyn Booru> {
        Box::new(self.clone())
    }
    fn next_page(&self, current_page: u32, _last_result_count: u32) -> u32 {
        current_page + 1
    }
    fn check_api_by_domain(&self, hash: u64, _domain: &str) -> bool {
        // TODO enforce www??
        hash == 1672757984210032119 || hash == 8889129358744041396
    }
    fn get_domain(&self) -> &String {
        &self.domain
    }
    fn imp(&self) -> Boorus {
        Boorus::Zerobooru(self.clone())
    }
}

impl Zerobooru {
    pub fn new(domain: String) -> Zerobooru {
        Zerobooru { domain }
    }

    pub async fn fetch_index(
        &self,
        search: &str,
        pid: u32,
    ) -> Result<std::vec::Vec<Box<dyn Post>>> {
        let url = [
            "https://",
            &self.domain[..],
            "/",
            &search.replace('_', "+").replace(' ', ","),
            "?json",
            &format!("&p={}", pid + 1),
        ]
        .join("");
        println!("fetching {}", url);
        let r = http_client_zerobooru().get(&url).send().await?;
        let posts = r.json::<Wrapper>().await?;
        Ok(posts
            .items
            .into_iter()
            .map(|mut p| {
                p.domain = self.domain.clone();
                p
            })
            .map(|mut p| {
                p.tags = p
                    .tags
                    .into_iter()
                    .map(|t| standard_tag_format(&t))
                    .collect();
                p.domain = self.domain.clone(); // TODO just ref, but lifetimes are hard
                Box::<dyn Post>::from(p)
            })
            .collect())
    }
}