use anyhow::{Context, Result};
use roxmltree::Document;
use super::{Ask, Category, Found, Indexer, Torrent, encode, magnet_link, parse_size};
pub struct Nyaa;
const BASE: &str = "https://nyaa.si/";
const SUBTITLED: &str = "1_2";
impl Indexer for Nyaa {
fn name(&self) -> &'static str {
"nyaa"
}
fn categories(&self) -> &'static [Category] {
&[Category::Anime]
}
fn urls(&self, ask: &Ask) -> Vec<String> {
let url = match ask {
Ask::Words(query) => format!("{BASE}?page=rss&q={}", encode(query)),
Ask::Browse(_) => format!("{BASE}?page=rss&c={SUBTITLED}"),
};
vec![url]
}
fn parse(&self, body: &str, _ask: &Ask) -> Result<Vec<Found>> {
let feed = Document::parse(body).context("nyaa sent something other than RSS")?;
let mut found = Vec::new();
for item in feed.descendants().filter(|node| node.has_tag_name("item")) {
let field = |name: &str| {
item.children()
.find(|child| child.tag_name().name() == name)
.and_then(|child| child.text())
.unwrap_or_default()
.trim()
};
let title = field("title");
let info_hash = field("infoHash").to_lowercase();
if title.is_empty() || info_hash.is_empty() {
continue;
}
found.push(Found::Ready(Torrent {
magnet: magnet_link(&info_hash, title),
title: title.to_string(),
size_bytes: parse_size(field("size")),
seeders: field("seeders").parse().unwrap_or(0),
leechers: field("leechers").parse().unwrap_or(0),
category: Category::Anime,
source: "nyaa",
info_hash,
}));
}
Ok(found)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::ready;
const FIXTURE: &str = include_str!("fixtures/nyaa.xml");
fn parse(body: &str) -> Vec<Torrent> {
ready(
Nyaa.parse(body, &Ask::Words("one piece".to_string()))
.unwrap(),
)
}
#[test]
fn a_saved_feed_becomes_torrents() {
let found = parse(FIXTURE);
assert_eq!(found.len(), 3);
let first = &found[0];
assert_eq!(
first.title,
"[KiyoshiiSubs] One Piece - 1173v2 [1080p][H.265 - 10Bit].mkv"
);
assert_eq!(first.seeders, 140);
assert_eq!(first.leechers, 4);
assert_eq!(first.info_hash, "7f323023040a468749fb2336350588c17907522f");
assert_eq!(first.size_bytes, 852_282_572);
assert!(first.magnet.contains(&first.info_hash));
}
#[test]
fn an_item_with_no_hash_is_skipped_not_fatal() {
let feed = "<rss><channel><item><title>No hash here</title></item></channel></rss>";
assert!(parse(feed).is_empty());
}
#[test]
fn a_broken_feed_says_so() {
assert!(
Nyaa.parse("not xml at all", &Ask::Browse(Category::Anime))
.is_err()
);
}
#[test]
fn browsing_asks_for_the_shelf_rather_than_for_words() {
let browse = Nyaa.urls(&Ask::Browse(Category::Anime));
assert!(browse[0].contains(SUBTITLED));
assert!(!browse[0].contains("&q="));
}
}