misc improvements, improve documentation, switch numeric IDs to ints, and add search() stub
This commit is contained in:
parent
6fb454fcea
commit
bf0c84f126
6 changed files with 144 additions and 50 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1277,6 +1277,7 @@ name = "torznab-toolkit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
|
"lazy_static",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"xml-rs",
|
"xml-rs",
|
||||||
|
|
|
@ -5,6 +5,10 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-rt = "2.10.0"
|
actix-rt = "2.10.0"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
rocket = "0.5.1"
|
rocket = "0.5.1"
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
xml-rs = "0.8.23"
|
xml-rs = "0.8.23"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
|
115
src/api.rs
115
src/api.rs
|
@ -2,31 +2,43 @@
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
|
|
||||||
use crate::data::*;
|
use crate::data::*;
|
||||||
|
use crate::dummy::create_empty_config;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::response::status;
|
use rocket::response::status;
|
||||||
use rocket::{get, response::content::RawXml};
|
use rocket::{get, response::content::RawXml};
|
||||||
use xml::writer::{EmitterConfig, XmlEvent};
|
use xml::writer::{EmitterConfig, XmlEvent};
|
||||||
|
|
||||||
/// Holds the config for torznab-toolkit.
|
// Holds the config for torznab-toolkit.
|
||||||
///
|
//
|
||||||
/// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional.
|
// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional.
|
||||||
///
|
//
|
||||||
/// <div class="warning">It's required to be set to <i>something</i>, which is why it's an Option set to None.
|
// <div class="warning">It's required to be set to <i>something</i>, which is why it's an Option set to None.
|
||||||
///
|
//
|
||||||
/// However, this is NOT optional, and attempting to do anything with CONFIG not set will return an `Err`.</div>
|
// However, this is NOT optional, and attempting to do anything with CONFIG not set will return an `Err`.</div>
|
||||||
pub(crate) static mut CONFIG: Option<Config> = None;
|
|
||||||
|
|
||||||
/// Capabilities API endpoint (`/api?t=caps`)
|
pub(crate) static mut CONFIG: Option<Config> = None;
|
||||||
// FIXME: VERY incomplete
|
lazy_static! {
|
||||||
// TODO: Finish it (duh) and add optional apikey
|
static ref STATUS_CONFIG_NOT_SPECIFIED: status::Custom<RawXml<String>> = status::Custom(
|
||||||
#[get("/api?t=caps")]
|
|
||||||
pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
|
||||||
unsafe {
|
|
||||||
if CONFIG.is_none() {
|
|
||||||
return status::Custom(
|
|
||||||
Status::InternalServerError,
|
Status::InternalServerError,
|
||||||
RawXml("500 Internal server error: Config not specified".to_string()),
|
RawXml("500 Internal server error: Config not specified".to_string()),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 conf = create_empty_config();
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
let conf: Config = CONFIG.clone().ok_or("").unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,3 +52,74 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
||||||
writer.write(XmlEvent::end_element()).unwrap();
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string()));
|
return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/api?t=search&<form..>")]
|
||||||
|
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 conf = create_empty_config();
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
let conf: Config = CONFIG.clone().ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Clean up this code - split it into a separate function?
|
||||||
|
let mut apikey: String = "".to_string();
|
||||||
|
if !form.apikey.is_none() {
|
||||||
|
apikey = form.apikey.ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut categories: Vec<u32> = Vec::new();
|
||||||
|
if !form.cat.is_none() {
|
||||||
|
// unholy amalgation of code to make the comma-separated list of strings into a vector of integers
|
||||||
|
categories = form
|
||||||
|
.cat
|
||||||
|
.ok_or("")
|
||||||
|
.unwrap()
|
||||||
|
.split(",")
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extended_attribute_names: String = "".to_string();
|
||||||
|
if !form.attrs.is_none() {
|
||||||
|
extended_attribute_names = form.attrs.ok_or("").unwrap().split(",").collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extended_attrs: bool = false;
|
||||||
|
if !form.extended.is_none() && form.extended.ok_or(false).unwrap() == 1 {
|
||||||
|
extended_attrs = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset: u32 = 0;
|
||||||
|
if !form.offset.is_none() {
|
||||||
|
offset = form.offset.ok_or(0).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut limit: u32 = 0;
|
||||||
|
limit = conf.caps.limits.max;
|
||||||
|
let wanted_limit = form.limit.ok_or(limit).unwrap();
|
||||||
|
if wanted_limit < limit {
|
||||||
|
limit = wanted_limit
|
||||||
|
}
|
||||||
|
|
||||||
|
match conf.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
if !auth(apikey).unwrap() {
|
||||||
|
return status::Custom(
|
||||||
|
Status::Unauthorized,
|
||||||
|
RawXml("401 Unauthorized".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status::Custom(
|
||||||
|
Status::NotImplemented,
|
||||||
|
RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
63
src/data.rs
63
src/data.rs
|
@ -1,4 +1,6 @@
|
||||||
//! Contains tons of structs used by the library
|
//! Contains tons of structs used by the library
|
||||||
|
use rocket::FromForm;
|
||||||
|
|
||||||
pub(crate) type AuthFunc = fn(String) -> Result<bool, String>;
|
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>;
|
||||||
|
@ -25,14 +27,14 @@ pub struct ServerInfo {
|
||||||
/// `default` is the default number of results the program will return
|
/// `default` is the default number of results the program will return
|
||||||
pub struct Limits {
|
pub struct Limits {
|
||||||
/*
|
/*
|
||||||
I don't know why this would possibly need to be a u64, I can't imagine you'll be returning 18 quintillion results or whatever
|
I don't know why this would possibly need to be a u32, I can't imagine you'll be returning 4 billion results or whatever
|
||||||
In fact, I *really* hope you aren't - if you are, you're doing something extremely wrong
|
In fact, I *really* hope you aren't - if you are, you're doing something extremely wrong
|
||||||
But hey, it's an option
|
But hey, it's an option
|
||||||
*/
|
*/
|
||||||
/// The maximum number of entries that can be listed in a search query
|
/// The maximum number of entries that can be listed in a search query
|
||||||
pub max: u64,
|
pub max: u32,
|
||||||
/// The default number of entries to be listed in a search query
|
/// The default number of entries to be listed in a search query
|
||||||
pub default: u64,
|
pub default: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -52,7 +54,7 @@ pub struct Subcategory {
|
||||||
/// The numeric ID of a subcategory
|
/// The numeric ID of a subcategory
|
||||||
///
|
///
|
||||||
/// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Category
|
/// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Category
|
||||||
pub id: String,
|
pub id: u32,
|
||||||
/// The name of the subcategory, e.g. "Anime" under the "TV" cateogyr
|
/// The name of the subcategory, e.g. "Anime" under the "TV" cateogyr
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ pub struct Category {
|
||||||
/// The numeric ID of a category
|
/// The numeric ID of a category
|
||||||
///
|
///
|
||||||
/// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Subcategory
|
/// The (de facto?) standard is `xxyy`, xx being the first two digits of the category, and the last two digits specifying the subcategory; see also: Subcategory
|
||||||
pub id: String,
|
pub id: u32,
|
||||||
/// The name of the category, e.g. "Movies"
|
/// The name of the category, e.g. "Movies"
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// A vector of all the subcategory in this category
|
/// A vector of all the subcategory in this category
|
||||||
|
@ -75,10 +77,10 @@ pub struct Category {
|
||||||
pub struct Genre {
|
pub struct Genre {
|
||||||
/// The numeric ID of a genre
|
/// The numeric ID of a genre
|
||||||
///
|
///
|
||||||
/// I'm not aware of any sure standard for this; the specification for Torznab shows an example with an ID of 1.
|
/// I'm not aware of any standard for numbering this; the specification for Torznab shows an example with an ID of 1.
|
||||||
pub id: String,
|
pub id: u32,
|
||||||
/// The numeric ID of the category this genre is for.
|
/// The numeric ID of the category this genre is for.
|
||||||
pub category_id: String,
|
pub category_id: u32,
|
||||||
/// The name of the genre
|
/// The name of the genre
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
@ -93,27 +95,11 @@ pub struct Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Holds the configuration for the capabilities of the Torznab server
|
/// Holds the configuration for the capabilities of the Torznab server (used in `/api?t=caps`)
|
||||||
///
|
|
||||||
/// - server_info: `ServerInfo`
|
|
||||||
/// - see: `ServerInfo` docs
|
|
||||||
/// - limits: `Limits`
|
|
||||||
/// - specifies the max and default items listed when searching
|
|
||||||
/// - see: `Limits` docs
|
|
||||||
/// - searching: `Vec<SearchInfo>`
|
|
||||||
/// - specifies the capabilities of each search mode
|
|
||||||
/// - see: `SearchInfo` docs
|
|
||||||
/// - categories: `Vec<Category>`
|
|
||||||
/// - lists known categories
|
|
||||||
/// - see: `Category` docs
|
|
||||||
/// - genres: `Option<Vec<Genre>>`
|
|
||||||
/// - lists known genres, optional
|
|
||||||
/// - see: `Genre` docs
|
|
||||||
///
|
///
|
||||||
/// <div class="warning">Note that this library might not support all the capabilities listed in yet, so check the README before listing capabilities, or just accept that unsupported capabilities will return error 404.
|
/// <div class="warning">Note that this library might not support all the capabilities listed in yet, so check the README before listing capabilities, or just accept that unsupported capabilities will return error 404.
|
||||||
///
|
///
|
||||||
/// It's recommended to add any capabilities you want, and set `available` to `false` in the `Caps` struct for any currently unsupported search types.</div>
|
/// It's recommended to add any capabilities you want, and set `available` to `false` in the [`Caps`] struct for any currently unsupported search types.</div>
|
||||||
///
|
|
||||||
///
|
///
|
||||||
/// 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 {
|
||||||
|
@ -127,7 +113,7 @@ pub struct Caps {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// A struct that holds configuration for torznab-toolkit
|
/// A struct that holds configuration for torznab-toolkit
|
||||||
/// A search function (/api?t=search) and capabilities (/api?t=caps - struct Caps) required
|
/// The search function (`/api?t=search`) and capabilities (`/api?t=caps` - struct [`Caps`]) are required
|
||||||
/// Everything else is optional
|
/// Everything else is optional
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub search: SearchFunc,
|
pub search: SearchFunc,
|
||||||
|
@ -138,3 +124,26 @@ pub struct Config {
|
||||||
pub music: Option<SearchFunc>,
|
pub music: Option<SearchFunc>,
|
||||||
pub book: Option<SearchFunc>,
|
pub book: Option<SearchFunc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, FromForm)]
|
||||||
|
/// A struct used by the API's search functions to hold its query parameters
|
||||||
|
/// Currently required (AFAIK) because of limitations with rocket
|
||||||
|
pub(crate) struct SearchForm {
|
||||||
|
/// The text query for the search
|
||||||
|
pub(crate) q: Option<String>,
|
||||||
|
/// The apikey, for authentication
|
||||||
|
pub(crate) apikey: Option<String>,
|
||||||
|
/// The list of numeric category IDs to be included in the search results
|
||||||
|
/// Returned by Rocket.rs as a string of comma-separated values, then split in the function to a `Vec<u32>`
|
||||||
|
pub(crate) cat: Option<String>,
|
||||||
|
/// The list of extended attribute names to be included in the search results
|
||||||
|
/// Returned by Rocket.rs as a string of comma-separated values, then split in the function to a `Vec<String>`
|
||||||
|
pub(crate) attrs: Option<String>,
|
||||||
|
/// Whether *all* extended attributes should be included in the search results; overrules `attrs`
|
||||||
|
/// Can be 0 or 1
|
||||||
|
pub(crate) extended: Option<u8>,
|
||||||
|
/// How many items to skip/offset by in the results.
|
||||||
|
pub(crate) offset: Option<u32>,
|
||||||
|
/// The maximum number of items to return - also limited to whatever `limits` is in [`Caps`]
|
||||||
|
pub(crate) limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
17
src/dummy.rs
17
src/dummy.rs
|
@ -10,7 +10,7 @@ fn dummy_auth_func(_a: String) -> Result<bool, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a bare-minimum config
|
/// Creates a bare-minimum config
|
||||||
fn create_empty_config() -> Config {
|
pub(crate) fn create_empty_config() -> Config {
|
||||||
let mut searching = Vec::new();
|
let mut searching = Vec::new();
|
||||||
searching.push(SearchInfo {
|
searching.push(SearchInfo {
|
||||||
search_type: "search".to_string(),
|
search_type: "search".to_string(),
|
||||||
|
@ -20,21 +20,21 @@ fn create_empty_config() -> Config {
|
||||||
|
|
||||||
let mut subcategories = Vec::new();
|
let mut subcategories = Vec::new();
|
||||||
subcategories.push(Subcategory {
|
subcategories.push(Subcategory {
|
||||||
id: "a".to_string(),
|
id: 1010,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut categories = Vec::new();
|
let mut categories = Vec::new();
|
||||||
categories.push(Category {
|
categories.push(Category {
|
||||||
id: "a".to_string(),
|
id: 1000,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
subcategories: subcategories,
|
subcategories: subcategories,
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut genres = Vec::new();
|
let mut genres = Vec::new();
|
||||||
genres.push(Genre {
|
genres.push(Genre {
|
||||||
id: "a".to_string(),
|
id: 1,
|
||||||
category_id: "b".to_string(),
|
category_id: 1000,
|
||||||
name: "c".to_string(),
|
name: "c".to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,10 +54,7 @@ fn create_empty_config() -> Config {
|
||||||
image: None,
|
image: None,
|
||||||
version: Some("1.0".to_string()),
|
version: Some("1.0".to_string()),
|
||||||
},
|
},
|
||||||
limits: Limits {
|
limits: Limits { max: 1, default: 1 },
|
||||||
max: 100,
|
|
||||||
default: 20,
|
|
||||||
},
|
|
||||||
searching: searching,
|
searching: searching,
|
||||||
categories: categories,
|
categories: categories,
|
||||||
genres: Some(genres),
|
genres: Some(genres),
|
||||||
|
@ -102,7 +99,7 @@ mod tests {
|
||||||
// in this case, CONFIG is still None
|
// in this case, CONFIG is still None
|
||||||
// can't just use run() because that expects a Config, not an Option<Config>
|
// can't just use run() because that expects a Config, not an Option<Config>
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", rocket::routes![api::caps])
|
.mount("/", rocket::routes![api::caps, api::search])
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -30,7 +30,7 @@ pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> {
|
||||||
api::CONFIG = Some(conf);
|
api::CONFIG = Some(conf);
|
||||||
}
|
}
|
||||||
match rocket::build()
|
match rocket::build()
|
||||||
.mount("/", rocket::routes![api::caps])
|
.mount("/", rocket::routes![api::caps, api::search])
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue