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
|
//! 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();
|
||||||
|
|
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
|
// 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
|
||||||
|
|
59
src/dummy.rs
59
src/dummy.rs
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue