finish implementing the capabilities (caps
) api endpoint
This commit is contained in:
parent
8ad7ba87ac
commit
ccac5b9842
3 changed files with 158 additions and 53 deletions
130
src/api.rs
130
src/api.rs
|
@ -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();
|
||||
|
|
22
src/data.rs
22
src/data.rs
|
@ -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
|
||||
|
|
59
src/dummy.rs
59
src/dummy.rs
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue