diff --git a/Cargo.lock b/Cargo.lock
index a49fa02..d9da6bf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1277,6 +1277,7 @@ name = "torznab-toolkit"
version = "0.1.0"
dependencies = [
"actix-rt",
+ "lazy_static",
"rocket",
"serde",
"xml-rs",
diff --git a/Cargo.toml b/Cargo.toml
index ca3ef68..b404c62 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,10 @@ edition = "2021"
[dependencies]
actix-rt = "2.10.0"
+lazy_static = "1.5.0"
rocket = "0.5.1"
serde = { version = "1.0.215", features = ["derive"] }
xml-rs = "0.8.23"
+
+[profile.release]
+opt-level = 3
diff --git a/src/api.rs b/src/api.rs
index 0ff1284..86a2b90 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -2,31 +2,43 @@
use std::io::stdout;
use crate::data::*;
+use crate::dummy::create_empty_config;
+use lazy_static::lazy_static;
use rocket::http::Status;
use rocket::response::status;
use rocket::{get, response::content::RawXml};
use xml::writer::{EmitterConfig, XmlEvent};
-/// Holds the config for torznab-toolkit.
-///
-/// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional.
-///
-///
It's required to be set to something, 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`.
+// Holds the config for torznab-toolkit.
+//
+// A search function (`/api?t=search`) and capabilities (`/api?t=caps` - `Caps`) are required, everything else is optional.
+//
+// It's required to be set to something, 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`.
+
pub(crate) static mut CONFIG: Option = None;
+lazy_static! {
+ static ref STATUS_CONFIG_NOT_SPECIFIED: status::Custom> = status::Custom(
+ Status::InternalServerError,
+ 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
-// TODO: Finish it (duh) and add optional apikey
#[get("/api?t=caps")]
pub(crate) fn caps() -> status::Custom> {
+ // 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::Custom(
- Status::InternalServerError,
- RawXml("500 Internal server error: Config not specified".to_string()),
- );
+ 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> {
writer.write(XmlEvent::end_element()).unwrap();
return status::Custom(Status::Ok, RawXml(stringify!(writer).to_string()));
}
+
+#[get("/api?t=search&")]
+pub(crate) fn search(form: SearchForm) -> status::Custom> {
+ // 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 = 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()),
+ );
+}
diff --git a/src/data.rs b/src/data.rs
index e0bb97c..8f16e25 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -1,4 +1,6 @@
//! Contains tons of structs used by the library
+use rocket::FromForm;
+
pub(crate) type AuthFunc = fn(String) -> Result;
// TODO: Figure out what the arguments should be for a search function and what it should return
pub(crate) type SearchFunc = fn(String, Vec) -> Result;
@@ -25,14 +27,14 @@ pub struct ServerInfo {
/// `default` is the default number of results the program will return
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
But hey, it's an option
*/
/// 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
- pub default: u64,
+ pub default: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -52,7 +54,7 @@ pub struct 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
- pub id: String,
+ pub id: u32,
/// The name of the subcategory, e.g. "Anime" under the "TV" cateogyr
pub name: String,
}
@@ -63,7 +65,7 @@ pub struct 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
- pub id: String,
+ pub id: u32,
/// The name of the category, e.g. "Movies"
pub name: String,
/// A vector of all the subcategory in this category
@@ -75,10 +77,10 @@ pub struct Category {
pub struct 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.
- pub id: String,
+ /// I'm not aware of any standard for numbering this; the specification for Torznab shows an example with an ID of 1.
+ pub id: u32,
/// The numeric ID of the category this genre is for.
- pub category_id: String,
+ pub category_id: u32,
/// The name of the genre
pub name: String,
}
@@ -93,27 +95,11 @@ pub struct Tag {
}
#[derive(Debug, Clone, PartialEq, Eq)]
-/// Holds the configuration for the capabilities of the Torznab server
-///
-/// - server_info: `ServerInfo`
-/// - see: `ServerInfo` docs
-/// - limits: `Limits`
-/// - specifies the max and default items listed when searching
-/// - see: `Limits` docs
-/// - searching: `Vec`
-/// - specifies the capabilities of each search mode
-/// - see: `SearchInfo` docs
-/// - categories: `Vec`
-/// - lists known categories
-/// - see: `Category` docs
-/// - genres: `Option>`
-/// - lists known genres, optional
-/// - see: `Genre` docs
+/// Holds the configuration for the capabilities of the Torznab server (used in `/api?t=caps`)
///
/// 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.
-///
+/// It's recommended to add any capabilities you want, and set `available` to `false` in the [`Caps`] struct for any currently unsupported search types.
///
/// TODO: Add a way to partially(?) generate automatically from the Config
pub struct Caps {
@@ -127,7 +113,7 @@ pub struct Caps {
#[derive(Debug, Clone, PartialEq, Eq)]
/// 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
pub struct Config {
pub search: SearchFunc,
@@ -138,3 +124,26 @@ pub struct Config {
pub music: Option,
pub book: Option,
}
+
+#[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,
+ /// The apikey, for authentication
+ pub(crate) apikey: Option,
+ /// 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`
+ pub(crate) cat: Option,
+ /// 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`
+ pub(crate) attrs: Option,
+ /// Whether *all* extended attributes should be included in the search results; overrules `attrs`
+ /// Can be 0 or 1
+ pub(crate) extended: Option,
+ /// How many items to skip/offset by in the results.
+ pub(crate) offset: Option,
+ /// The maximum number of items to return - also limited to whatever `limits` is in [`Caps`]
+ pub(crate) limit: Option,
+}
diff --git a/src/dummy.rs b/src/dummy.rs
index 068dd76..4d61c99 100644
--- a/src/dummy.rs
+++ b/src/dummy.rs
@@ -10,7 +10,7 @@ fn dummy_auth_func(_a: String) -> Result {
}
/// Creates a bare-minimum config
-fn create_empty_config() -> Config {
+pub(crate) fn create_empty_config() -> Config {
let mut searching = Vec::new();
searching.push(SearchInfo {
search_type: "search".to_string(),
@@ -20,21 +20,21 @@ fn create_empty_config() -> Config {
let mut subcategories = Vec::new();
subcategories.push(Subcategory {
- id: "a".to_string(),
+ id: 1010,
name: "b".to_string(),
});
let mut categories = Vec::new();
categories.push(Category {
- id: "a".to_string(),
+ id: 1000,
name: "b".to_string(),
subcategories: subcategories,
});
let mut genres = Vec::new();
genres.push(Genre {
- id: "a".to_string(),
- category_id: "b".to_string(),
+ id: 1,
+ category_id: 1000,
name: "c".to_string(),
});
@@ -54,10 +54,7 @@ fn create_empty_config() -> Config {
image: None,
version: Some("1.0".to_string()),
},
- limits: Limits {
- max: 100,
- default: 20,
- },
+ limits: Limits { max: 1, default: 1 },
searching: searching,
categories: categories,
genres: Some(genres),
@@ -102,7 +99,7 @@ mod tests {
// in this case, CONFIG is still None
// can't just use run() because that expects a Config, not an Option
rocket::build()
- .mount("/", rocket::routes![api::caps])
+ .mount("/", rocket::routes![api::caps, api::search])
.launch()
.await
.unwrap();
diff --git a/src/lib.rs b/src/lib.rs
index b01bf93..b8c75ba 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,7 +30,7 @@ pub async fn run(conf: data::Config) -> Result {
api::CONFIG = Some(conf);
}
match rocket::build()
- .mount("/", rocket::routes![api::caps])
+ .mount("/", rocket::routes![api::caps, api::search])
.launch()
.await
{