switch from static mut
config to using Rocket's State
This commit is contained in:
parent
f2b559ab5f
commit
01c9984d99
6 changed files with 61 additions and 135 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1277,7 +1277,6 @@ 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,7 +5,6 @@ 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"
|
||||||
|
|
159
src/api.rs
159
src/api.rs
|
@ -1,10 +1,9 @@
|
||||||
//! Contains the actual Torznab API
|
//! Contains the actual Torznab API
|
||||||
use crate::data::*;
|
use crate::data::*;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::response::status;
|
use rocket::response::status;
|
||||||
use rocket::FromForm;
|
|
||||||
use rocket::{get, response::content::RawXml};
|
use rocket::{get, response::content::RawXml};
|
||||||
|
use rocket::{FromForm, State};
|
||||||
use std::str;
|
use std::str;
|
||||||
use xml::writer::{EmitterConfig, XmlEvent};
|
use xml::writer::{EmitterConfig, XmlEvent};
|
||||||
|
|
||||||
|
@ -81,41 +80,11 @@ impl SearchForm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holds the config for torznab-toolkit.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
lazy_static! {
|
|
||||||
static ref STATUS_CONFIG_NOT_SPECIFIED: status::Custom<RawXml<String>> = status::Custom(
|
|
||||||
Status::InternalServerError,
|
|
||||||
RawXml("500 Internal server error: Config not specified".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
||||||
#[get("/api?t=caps")]
|
#[get("/api?t=caps", rank = 1)]
|
||||||
pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
pub(crate) async fn caps(conf: &State<Config>) -> 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;
|
|
||||||
unsafe {
|
|
||||||
match CONFIG {
|
|
||||||
Some(ref config) => {
|
|
||||||
conf = config.clone();
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer = Vec::new();
|
let buffer = Vec::new();
|
||||||
let mut writer = EmitterConfig::new().create_writer(buffer);
|
let mut writer = EmitterConfig::new().create_writer(buffer);
|
||||||
|
|
||||||
|
@ -148,7 +117,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
|
|
||||||
// Add the search types
|
// Add the search types
|
||||||
writer.write(XmlEvent::start_element("searching")).unwrap();
|
writer.write(XmlEvent::start_element("searching")).unwrap();
|
||||||
for item in conf.caps.searching {
|
for item in &conf.caps.searching {
|
||||||
let mut available = "yes";
|
let mut available = "yes";
|
||||||
if !item.available {
|
if !item.available {
|
||||||
available = "no";
|
available = "no";
|
||||||
|
@ -165,7 +134,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
writer.write(XmlEvent::end_element()).unwrap(); // close `searching`
|
writer.write(XmlEvent::end_element()).unwrap(); // close `searching`
|
||||||
|
|
||||||
writer.write(XmlEvent::start_element("categories")).unwrap();
|
writer.write(XmlEvent::start_element("categories")).unwrap();
|
||||||
for i in conf.caps.categories {
|
for i in &conf.caps.categories {
|
||||||
writer
|
writer
|
||||||
.write(
|
.write(
|
||||||
XmlEvent::start_element("category")
|
XmlEvent::start_element("category")
|
||||||
|
@ -173,7 +142,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
.attr("name", i.name.as_str()),
|
.attr("name", i.name.as_str()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for j in i.subcategories {
|
for j in &i.subcategories {
|
||||||
writer
|
writer
|
||||||
.write(
|
.write(
|
||||||
XmlEvent::start_element("subcat")
|
XmlEvent::start_element("subcat")
|
||||||
|
@ -187,7 +156,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
}
|
}
|
||||||
writer.write(XmlEvent::end_element()).unwrap(); // close `categories`
|
writer.write(XmlEvent::end_element()).unwrap(); // close `categories`
|
||||||
|
|
||||||
match conf.caps.genres {
|
match &conf.caps.genres {
|
||||||
Some(genres) => {
|
Some(genres) => {
|
||||||
writer.write(XmlEvent::start_element("genres")).unwrap();
|
writer.write(XmlEvent::start_element("genres")).unwrap();
|
||||||
|
|
||||||
|
@ -207,7 +176,7 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match conf.caps.tags {
|
match &conf.caps.tags {
|
||||||
Some(tags) => {
|
Some(tags) => {
|
||||||
writer.write(XmlEvent::start_element("tags")).unwrap();
|
writer.write(XmlEvent::start_element("tags")).unwrap();
|
||||||
|
|
||||||
|
@ -233,21 +202,14 @@ pub(crate) async fn caps() -> status::Custom<RawXml<String>> {
|
||||||
return status::Custom(Status::Ok, RawXml(result));
|
return status::Custom(Status::Ok, RawXml(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=search&<form..>")]
|
#[get("/api?t=search&<form..>", rank = 2)]
|
||||||
/// The general search function
|
/// The general search function
|
||||||
pub(crate) async fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
pub(crate) async fn search(
|
||||||
// The compiler won't let you get a field from a struct in the Option here, since the default is None
|
conf: &State<Config>,
|
||||||
// So this is needed
|
form: SearchForm,
|
||||||
let conf;
|
) -> status::Custom<RawXml<String>> {
|
||||||
unsafe {
|
// oh god this is horrible but it works
|
||||||
if CONFIG.is_none() {
|
let parameters = form.to_parameters((**conf).clone());
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
} else {
|
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = form.to_parameters(conf.clone());
|
|
||||||
|
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
|
@ -276,21 +238,14 @@ pub(crate) async fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
||||||
return search_handler(conf, search_parameters).await;
|
return search_handler(conf, search_parameters).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=tvsearch&<form..>")]
|
#[get("/api?t=tvsearch&<form..>", rank = 3)]
|
||||||
/// The TV search function
|
/// The TV search function
|
||||||
pub(crate) async fn tv_search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
pub(crate) async fn tv_search(
|
||||||
// The compiler won't let you get a field from a struct in the Option here, since the default is None
|
conf: &State<Config>,
|
||||||
// So this is needed
|
form: SearchForm,
|
||||||
let conf;
|
) -> status::Custom<RawXml<String>> {
|
||||||
unsafe {
|
// oh god this is horrible but it works
|
||||||
if CONFIG.is_none() {
|
let parameters = form.to_parameters((**conf).clone());
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
} else {
|
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = form.to_parameters(conf.clone());
|
|
||||||
|
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
|
@ -326,21 +281,14 @@ pub(crate) async fn tv_search(form: SearchForm) -> status::Custom<RawXml<String>
|
||||||
return search_handler(conf, search_parameters).await;
|
return search_handler(conf, search_parameters).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=movie&<form..>")]
|
#[get("/api?t=movie&<form..>", rank = 4)]
|
||||||
/// The movie search function
|
/// The movie search function
|
||||||
pub(crate) async fn movie_search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
pub(crate) async fn movie_search(
|
||||||
// The compiler won't let you get a field from a struct in the Option here, since the default is None
|
conf: &State<Config>,
|
||||||
// So this is needed
|
form: SearchForm,
|
||||||
let conf;
|
) -> status::Custom<RawXml<String>> {
|
||||||
unsafe {
|
// oh god this is horrible but it works
|
||||||
if CONFIG.is_none() {
|
let parameters = form.to_parameters((**conf).clone());
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
} else {
|
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = form.to_parameters(conf.clone());
|
|
||||||
|
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
|
@ -376,21 +324,14 @@ pub(crate) async fn movie_search(form: SearchForm) -> status::Custom<RawXml<Stri
|
||||||
return search_handler(conf, search_parameters).await;
|
return search_handler(conf, search_parameters).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=music&<form..>")]
|
#[get("/api?t=music&<form..>", rank = 5)]
|
||||||
/// The music search function
|
/// The music search function
|
||||||
pub(crate) async fn music_search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
pub(crate) async fn music_search(
|
||||||
// The compiler won't let you get a field from a struct in the Option here, since the default is None
|
conf: &State<Config>,
|
||||||
// So this is needed
|
form: SearchForm,
|
||||||
let conf;
|
) -> status::Custom<RawXml<String>> {
|
||||||
unsafe {
|
// oh god this is horrible but it works
|
||||||
if CONFIG.is_none() {
|
let parameters = form.to_parameters((**conf).clone());
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
} else {
|
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = form.to_parameters(conf.clone());
|
|
||||||
|
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
|
@ -426,21 +367,14 @@ pub(crate) async fn music_search(form: SearchForm) -> status::Custom<RawXml<Stri
|
||||||
return search_handler(conf, search_parameters).await;
|
return search_handler(conf, search_parameters).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=book&<form..>")]
|
#[get("/api?t=book&<form..>", rank = 6)]
|
||||||
/// The music search function
|
/// The music search function
|
||||||
pub(crate) async fn book_search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
pub(crate) async fn book_search(
|
||||||
// The compiler won't let you get a field from a struct in the Option here, since the default is None
|
conf: &State<Config>,
|
||||||
// So this is needed
|
form: SearchForm,
|
||||||
let conf;
|
) -> status::Custom<RawXml<String>> {
|
||||||
unsafe {
|
// oh god this is horrible but it works
|
||||||
if CONFIG.is_none() {
|
let parameters = form.to_parameters((**conf).clone());
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
|
||||||
} else {
|
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = form.to_parameters(conf.clone());
|
|
||||||
|
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
|
@ -476,7 +410,10 @@ pub(crate) async fn book_search(form: SearchForm) -> status::Custom<RawXml<Strin
|
||||||
return search_handler(conf, search_parameters).await;
|
return search_handler(conf, search_parameters).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_handler(conf: Config, parameters: SearchParameters) -> status::Custom<RawXml<String>> {
|
async fn search_handler(
|
||||||
|
conf: &State<Config>,
|
||||||
|
parameters: SearchParameters,
|
||||||
|
) -> status::Custom<RawXml<String>> {
|
||||||
let buffer = Vec::new();
|
let buffer = Vec::new();
|
||||||
let mut writer = EmitterConfig::new().create_writer(buffer);
|
let mut writer = EmitterConfig::new().create_writer(buffer);
|
||||||
writer
|
writer
|
||||||
|
@ -499,7 +436,7 @@ async fn search_handler(conf: Config, parameters: SearchParameters) -> status::C
|
||||||
// add `title`
|
// add `title`
|
||||||
writer.write(XmlEvent::start_element("title")).unwrap();
|
writer.write(XmlEvent::start_element("title")).unwrap();
|
||||||
let mut title_provided = false;
|
let mut title_provided = false;
|
||||||
match conf.caps.server_info {
|
match &conf.caps.server_info {
|
||||||
Some(server_info) => {
|
Some(server_info) => {
|
||||||
if server_info.contains_key("title") {
|
if server_info.contains_key("title") {
|
||||||
match server_info.get("title") {
|
match server_info.get("title") {
|
||||||
|
|
17
src/dummy.rs
17
src/dummy.rs
|
@ -91,23 +91,6 @@ pub(crate) fn create_empty_config() -> Config {
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{api, dummy::create_empty_config, run};
|
use crate::{api, dummy::create_empty_config, run};
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn caps_test_with_empty_config() {
|
|
||||||
unsafe {
|
|
||||||
crate::api::CONFIG = Some(create_empty_config());
|
|
||||||
println!("{:?}", crate::api::CONFIG);
|
|
||||||
}
|
|
||||||
println!("{:?}", crate::api::caps().await);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn caps_test_no_config() {
|
|
||||||
unsafe {
|
|
||||||
println!("{:?}", crate::api::CONFIG);
|
|
||||||
}
|
|
||||||
println!("{:?}", crate::api::caps().await);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn api_with_empty_config() {
|
async fn api_with_empty_config() {
|
||||||
run(create_empty_config()).await.unwrap();
|
run(create_empty_config()).await.unwrap();
|
||||||
|
|
16
src/lib.rs
16
src/lib.rs
|
@ -8,11 +8,19 @@ use rocket::{self};
|
||||||
|
|
||||||
/// Runs the server
|
/// Runs the server
|
||||||
pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> {
|
pub async fn run(conf: data::Config) -> Result<bool, rocket::Error> {
|
||||||
unsafe {
|
|
||||||
api::CONFIG = Some(conf);
|
|
||||||
}
|
|
||||||
match rocket::build()
|
match rocket::build()
|
||||||
.mount("/", rocket::routes![api::caps, api::search])
|
.mount(
|
||||||
|
"/",
|
||||||
|
rocket::routes![
|
||||||
|
api::caps,
|
||||||
|
api::search,
|
||||||
|
api::tv_search,
|
||||||
|
api::movie_search,
|
||||||
|
api::music_search,
|
||||||
|
api::book_search
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.manage(conf)
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
//! - Please implement the `season`, `ep`, and `id` attributes for torrents when possible
|
//! - Please implement the `season`, `ep`, and `id` attributes for torrents when possible
|
||||||
//! - Implementing `id`, at least, is far out of scope of this library, and providing `season` and `ep` more effective than this library parsing for them. However, parsing for those as an optional fallback may be added later.
|
//! - Implementing `id`, at least, is far out of scope of this library, and providing `season` and `ep` more effective than this library parsing for them. However, parsing for those as an optional fallback may be added later.
|
||||||
//! - See [here](https://torznab.github.io/spec-1.3-draft/revisions/1.0-Torznab-Torrent-Support.html) for details
|
//! - See [here](https://torznab.github.io/spec-1.3-draft/revisions/1.0-Torznab-Torrent-Support.html) for details
|
||||||
//!
|
//!
|
||||||
//! TODO: Add better docs for using the library
|
//! TODO: Add better docs for using the library
|
||||||
|
|
||||||
// TODO: Add parsing for `season` and `ep`
|
// TODO: Add parsing for `season` and `ep`
|
||||||
|
|
Loading…
Reference in a new issue