finish implementing the capabilities (caps) api endpoint

This commit is contained in:
askiiart 2024-11-30 01:12:37 -06:00
parent 8ad7ba87ac
commit ccac5b9842
Signed by untrusted user who does not match committer: askiiart
GPG key ID: EA85979611654C30
3 changed files with 158 additions and 53 deletions

View file

@ -1,6 +1,4 @@
//! Contains the actual Torznab API
use std::io::stdout;
use crate::data::*;
use crate::dummy::create_empty_config;
use lazy_static::lazy_static;
@ -8,7 +6,7 @@ use rocket::http::Status;
use rocket::response::status;
use rocket::FromForm;
use rocket::{get, response::content::RawXml};
use std::collections::HashMap;
use std::str;
use xml::writer::{EmitterConfig, XmlEvent};
#[derive(Debug, Clone, PartialEq, Eq, FromForm)]
@ -106,12 +104,11 @@ lazy_static! {
/// Capabilities API endpoint (`/api?t=caps`)
///
/// Note that an apikey is *not* required for this function, regardless of whether it's required for the rest.
// FIXME: VERY incomplete
#[get("/api?t=caps")]
pub(crate) fn caps() -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let mut conf = create_empty_config();
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
@ -120,15 +117,122 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
}
}
let output = stdout();
let mut writer = EmitterConfig::new().create_writer(output);
let buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(buffer);
writer.write(XmlEvent::start_element("caps")).unwrap();
writer.write(XmlEvent::start_element("server")).unwrap();
writer.write(XmlEvent::end_element()).unwrap();
writer.write(XmlEvent::start_element("caps")).unwrap();
writer.write(XmlEvent::end_element()).unwrap();
return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string()));
// add the server info
// TODO: Clean up the code by making the elements a Vec (to be used as a stack), rather than manually keeping track of them
let mut element = XmlEvent::start_element("server");
match &conf.caps.server_info {
Some(server_info) => {
// needs to be a vec since if i just `.as_str()` them, they don't live long enough
let server_info_vec: Vec<(&String, &String)> = server_info.iter().collect();
for (key, value) in server_info_vec {
element = element.attr(key.as_str(), value.as_str());
}
}
None => {}
}
writer.write(element).unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // close `server`
// add the limits
writer
.write(
XmlEvent::start_element("limits")
.attr("max", conf.caps.limits.max.to_string().as_str())
.attr("default", conf.caps.limits.default.to_string().as_str()),
)
.unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // close `limits`
// Add the search types
writer.write(XmlEvent::start_element("searching")).unwrap();
for item in conf.caps.searching {
let mut available = "yes";
if !item.available {
available = "no";
}
writer
.write(
XmlEvent::start_element(item.search_type.as_str())
.attr("available", available)
.attr("supportedParams", item.supported_params.join(",").as_str()),
)
.unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // close element
}
writer.write(XmlEvent::end_element()).unwrap(); // close `searching`
writer.write(XmlEvent::start_element("categories")).unwrap();
for i in conf.caps.categories {
writer
.write(
XmlEvent::start_element("category")
.attr("id", i.id.to_string().as_str())
.attr("name", i.name.as_str()),
)
.unwrap();
for j in i.subcategories {
writer
.write(
XmlEvent::start_element("subcat")
.attr("id", j.id.to_string().as_str())
.attr("name", j.name.as_str()),
)
.unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // close `subcat` element
}
writer.write(XmlEvent::end_element()).unwrap(); // close `category` element
}
writer.write(XmlEvent::end_element()).unwrap(); // close `categories`
match conf.caps.genres {
Some(genres) => {
writer.write(XmlEvent::start_element("genres")).unwrap();
for genre in genres {
writer
.write(
XmlEvent::start_element("genre")
.attr("id", genre.id.to_string().as_str())
.attr("categoryid", genre.category_id.to_string().as_str())
.attr("name", genre.name.as_str()),
)
.unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // close `genre` element
}
writer.write(XmlEvent::end_element()).unwrap(); // close `genres` element
}
None => {}
}
match conf.caps.tags {
Some(tags) => {
writer.write(XmlEvent::start_element("tags")).unwrap();
for tag in tags {
writer
.write(
XmlEvent::start_element("tag")
.attr("name", tag.name.as_str())
.attr("description", tag.description.as_str()),
)
.unwrap();
}
writer.write(XmlEvent::end_element()).unwrap(); // close `tags` element
}
None => {}
}
writer.write(XmlEvent::end_element()).unwrap(); // close `caps`
let result = str::from_utf8(writer.into_inner().as_slice())
.unwrap()
.to_string(); // Convert buffer to a String
return status::Custom(Status::Ok, RawXml(result));
}
#[get("/api?t=search&<form..>")]
@ -137,7 +241,7 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
pub(crate) fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
// The compiler won't let you get a field from a struct in the Option here, since the default is None
// So this is needed
let mut conf = create_empty_config();
let conf;
unsafe {
if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();

View file

@ -7,22 +7,6 @@ pub(crate) type AuthFunc = fn(String) -> Result<bool, String>;
// TODO: Figure out what the arguments should be for a search function and what it should return
pub(crate) type SearchFunc = fn(String, Vec<String>) -> Result<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
/// Specify the ServerInfo to be listed in <server> for `/api?t=caps`
///
/// These fields are just those listed in the example on [torznab.github.io](https://torznab.github.io), there's no actual specification for thse fields.
/// TODO: Update this to have customizable fields instead
pub struct ServerInfo {
/// The title of the server
pub title: Option<String>,
/// The email for the server info
pub email: Option<String>,
/// The URL to the server's image (e.g. logo)
pub image: Option<String>,
/// What version the server is - unrelated to torznab-toolkit's version, but may be used by the program
pub version: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// The maximum and defaults for the `limit` parameter in queries
/// `max` is the maximum number of results the program can return
@ -47,6 +31,8 @@ pub struct SearchInfo {
/// Whether this search type is available
pub available: bool,
/// The supported parameters for this search type
///
/// Highly recommended: `q` (free text query)
pub supported_params: Vec<String>,
}
@ -106,7 +92,9 @@ pub struct Tag {
/// TODO: Add a way to partially(?) generate automatically from the Config
pub struct Caps {
/// The server info, like title - optional
pub server_info: Option<ServerInfo>,
///
/// Examples: `version`, `title`, `email`, `url`, `image`
pub server_info: Option<HashMap<String, String>>,
/// The max and default number of items to be returned by queries - see [`Limits`]
pub limits: Limits,
/// Info about each type of search

View file

@ -1,5 +1,24 @@
//! Some dummy stuff for testing the API
use crate::data::*;
use std::collections::HashMap;
#[macro_export]
/// HashMap equivalent of vec![]
///
/// Example:
/// ```rs
/// hashmap!(("key", "value"))
/// ```
macro_rules! hashmap {
($(($value1:expr, $value2:expr)),*) => {{
let mut hm = HashMap::new();
$(
hm.insert($value1, $value2);
)*
hm
}};
}
fn dummy_search_func(_a: String, _b: Vec<String>) -> Result<String, String> {
return Ok("hi".to_string());
@ -11,49 +30,43 @@ fn dummy_auth_func(_a: String) -> Result<bool, String> {
/// Creates a bare-minimum config
pub(crate) fn create_empty_config() -> Config {
let mut searching = Vec::new();
searching.push(SearchInfo {
let searching = vec![SearchInfo {
search_type: "search".to_string(),
available: true,
supported_params: vec!["id".to_string()],
});
supported_params: vec!["q".to_string()],
}];
let mut subcategories = Vec::new();
subcategories.push(Subcategory {
let subcategories = vec![Subcategory {
id: 1010,
name: "b".to_string(),
});
}];
let mut categories = Vec::new();
categories.push(Category {
let categories = vec![Category {
id: 1000,
name: "b".to_string(),
name: "a".to_string(),
subcategories: subcategories,
});
}];
let mut genres = Vec::new();
genres.push(Genre {
let genres = vec![Genre {
id: 1,
category_id: 1000,
name: "c".to_string(),
});
}];
let mut tags = Vec::new();
tags.push(Tag {
let tags = vec![Tag {
name: "a".to_string(),
description: "b".to_string(),
});
}];
return Config {
search: dummy_search_func,
auth: Some(dummy_auth_func),
caps: Caps {
server_info: Some(ServerInfo {
title: Some("Test Torznab server".to_string()),
email: Some("test@example.com".to_string()),
image: None,
version: Some("1.0".to_string()),
}),
server_info: Some(hashmap!(
("title".to_string(), "Test Torznab server".to_string()),
("email".to_string(), "test@example.com".to_string()),
("version".to_string(), "1.0".to_string())
)),
limits: Limits {
max: 100,
default: 20,