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 //! Contains the actual Torznab API
use std::io::stdout;
use crate::data::*; use crate::data::*;
use crate::dummy::create_empty_config; use crate::dummy::create_empty_config;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -8,7 +6,7 @@ use rocket::http::Status;
use rocket::response::status; use rocket::response::status;
use rocket::FromForm; use rocket::FromForm;
use rocket::{get, response::content::RawXml}; use rocket::{get, response::content::RawXml};
use std::collections::HashMap; use std::str;
use xml::writer::{EmitterConfig, XmlEvent}; use xml::writer::{EmitterConfig, XmlEvent};
#[derive(Debug, Clone, PartialEq, Eq, FromForm)] #[derive(Debug, Clone, PartialEq, Eq, FromForm)]
@ -106,12 +104,11 @@ lazy_static! {
/// Capabilities API endpoint (`/api?t=caps`) /// 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. /// 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")] #[get("/api?t=caps")]
pub(crate) fn caps() -> status::Custom<RawXml<String>> { 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 // 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 // So this is needed
let mut conf = create_empty_config(); let conf;
unsafe { unsafe {
if CONFIG.is_none() { if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone(); return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
@ -120,15 +117,122 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
} }
} }
let output = stdout(); let buffer = Vec::new();
let mut writer = EmitterConfig::new().create_writer(output); let mut writer = EmitterConfig::new().create_writer(buffer);
writer.write(XmlEvent::start_element("caps")).unwrap(); writer.write(XmlEvent::start_element("caps")).unwrap();
writer.write(XmlEvent::start_element("server")).unwrap();
writer.write(XmlEvent::end_element()).unwrap(); // add the server info
writer.write(XmlEvent::start_element("caps")).unwrap(); // TODO: Clean up the code by making the elements a Vec (to be used as a stack), rather than manually keeping track of them
writer.write(XmlEvent::end_element()).unwrap(); let mut element = XmlEvent::start_element("server");
return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string())); 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..>")] #[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>> { 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 // 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 // So this is needed
let mut conf = create_empty_config(); let conf;
unsafe { unsafe {
if CONFIG.is_none() { if CONFIG.is_none() {
return (*STATUS_CONFIG_NOT_SPECIFIED).clone(); 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 // 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>; 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)] #[derive(Debug, Clone, PartialEq, Eq)]
/// The maximum and defaults for the `limit` parameter in queries /// The maximum and defaults for the `limit` parameter in queries
/// `max` is the maximum number of results the program can return /// `max` is the maximum number of results the program can return
@ -47,6 +31,8 @@ pub struct SearchInfo {
/// Whether this search type is available /// Whether this search type is available
pub available: bool, pub available: bool,
/// The supported parameters for this search type /// The supported parameters for this search type
///
/// Highly recommended: `q` (free text query)
pub supported_params: Vec<String>, pub supported_params: Vec<String>,
} }
@ -106,7 +92,9 @@ pub struct Tag {
/// TODO: Add a way to partially(?) generate automatically from the Config /// TODO: Add a way to partially(?) generate automatically from the Config
pub struct Caps { pub struct Caps {
/// The server info, like title - optional /// 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`] /// The max and default number of items to be returned by queries - see [`Limits`]
pub limits: Limits, pub limits: Limits,
/// Info about each type of search /// Info about each type of search

View file

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