magnetease 0.3.1

A library to fetch magnets from the internet
Documentation
use async_trait::async_trait;
use reqwest::Client;
use scraper::{Html, Selector};

use crate::{
    Magnet, MagneteaseError, MagneteaseErrorKind, MagneteaseResult, Provider, ProviderCategory,
    WhichProvider,
};

pub struct Nyaa;

const URL: &str = "https://nyaa.si/";

#[async_trait]
impl Provider for Nyaa {
    fn name(&self) -> &'static str {
        "Nyaa"
    }

    fn display_url(&self) -> &'static str {
        "nyaa.si"
    }

    fn category(&self) -> ProviderCategory {
        ProviderCategory::Anime
    }

    async fn search(
        &self,
        client: &Client,
        phrase: &str,
    ) -> Result<MagneteaseResult, MagneteaseError> {
        let params = [("q", phrase)];
        let html = client
            .get(URL)
            .query(&params)
            .send()
            .await
            .map_err(|e| MagneteaseError {
                kind: e.into(),
                provider: WhichProvider::Nyaa,
            })?
            .text()
            .await
            .map_err(|e| MagneteaseError {
                kind: e.into(),
                provider: WhichProvider::Nyaa,
            })?;

        if html.contains("No results found") {
            return Ok(MagneteaseResult {
                provider: WhichProvider::Nyaa,
                magnets: vec![],
            });
        }

        let doc = Html::parse_document(&html);
        let torrent_table_selector = Selector::parse("tbody").unwrap();

        let torrent_table = doc
            .select(&torrent_table_selector)
            .next()
            .ok_or(MagneteaseError {
                kind: MagneteaseErrorKind::HtmlError,
                provider: WhichProvider::Nyaa,
            })?;

        let anchor_selector = Selector::parse("a").unwrap();
        let torrent_selector = Selector::parse("tr").unwrap();

        let mut magnets = vec![];

        for torrent in torrent_table.select(&torrent_selector) {
            let torrent_info: Vec<_> = torrent.child_elements().collect();

            let name = torrent_info
                .get(1)
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .select(&anchor_selector)
                .last()
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .inner_html();

            let url = torrent_info
                .get(2)
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .select(&anchor_selector)
                .last()
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .attr("href")
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .to_string();

            let bytes = size_to_bytes(
                &torrent_info
                    .get(3)
                    .ok_or(MagneteaseError {
                        kind: MagneteaseErrorKind::HtmlError,
                        provider: WhichProvider::Nyaa,
                    })?
                    .inner_html(),
            );

            let seeders: u32 = torrent_info
                .get(5)
                .ok_or(MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?
                .inner_html()
                .parse()
                .map_err(|_| MagneteaseError {
                    kind: MagneteaseErrorKind::HtmlError,
                    provider: WhichProvider::Nyaa,
                })?;

            let magnet = NyaaMagnet {
                url,
                name,
                seeders,
                bytes,
            };

            magnets.push(magnet);
        }
        let magnets = magnets
            .into_iter()
            .map(|nyaa_magnet| nyaa_magnet.into())
            .collect();

        Ok(MagneteaseResult {
            provider: WhichProvider::Nyaa,
            magnets,
        })
    }
}

pub struct NyaaMagnet {
    url: String,
    name: String,
    seeders: u32,
    bytes: u64,
}

impl From<NyaaMagnet> for Magnet {
    fn from(value: NyaaMagnet) -> Self {
        Magnet {
            title: value.name,
            url: value.url,
            seeders: value.seeders,
            bytes: value.bytes,
        }
    }
}

fn size_to_bytes(size: &str) -> u64 {
    let kb = 1000.0;
    let mb = kb * 1000.0;
    let gb = mb * 1000.0;

    let (size, suffix) = size.split_once(' ').unwrap();

    let size: f64 = size.parse().unwrap();

    if suffix == "GiB" {
        (size * gb) as u64
    } else if suffix == "MiB" {
        (size * mb) as u64
    } else if suffix == "KiB" {
        (size * kb) as u64
    } else {
        todo!()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn nyaa_search() {
        let client = Client::new();
        let response = Nyaa.search(&client, "lain").await.unwrap();
        assert_eq!(response.magnets.len(), 75);
        assert!(!response.magnets[15].url.is_empty());
    }

    #[tokio::test]
    async fn nyaa_search_no_results() {
        let client = Client::new();
        let response = Nyaa.search(&client, "re 398 30 hfob q389by").await.unwrap();
        assert_eq!(response.magnets.len(), 0);
    }
}