complete all search functions
This commit is contained in:
parent
492579035d
commit
546e4944b4
6 changed files with 429 additions and 45 deletions
10
README.md
10
README.md
|
@ -24,11 +24,11 @@ Note: I wrote the line above when I was tired. Don't ask me what *literal* truck
|
||||||
| API call | Explanation | Implemented |
|
| API call | Explanation | Implemented |
|
||||||
| -------- | ------------------------------------------------------------ | ----------- |
|
| -------- | ------------------------------------------------------------ | ----------- |
|
||||||
| caps | Returns the capabilities of the api. | ✅ |
|
| caps | Returns the capabilities of the api. | ✅ |
|
||||||
| search | Free text search query. | ❌ |
|
| search | Free text search query. | ✅ |
|
||||||
| tvsearch | Search query with tv specific query params and filtering. | ❌ |
|
| tvsearch | Search query with tv specific query params and filtering. | ✅ |
|
||||||
| movie | Search query with movie specific query params and filtering. | ❌ |
|
| movie | Search query with movie specific query params and filtering. | ✅ |
|
||||||
| music | Search query with music specific query params and filtering. | ❌ |
|
| music | Search query with music specific query params and filtering. | ✅ |
|
||||||
| book | Search query with book specific query params and filtering. | ❌ |
|
| book | Search query with book specific query params and filtering. | ✅ |
|
||||||
|
|
||||||
<!-- for copy-pasting: ❌ ✅ -->
|
<!-- for copy-pasting: ❌ ✅ -->
|
||||||
(copied from [torznab.github.io](https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html))
|
(copied from [torznab.github.io](https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html))
|
||||||
|
|
|
@ -25,7 +25,7 @@ fn main() -> Result {
|
||||||
|
|
||||||
Queries are returned as an RSS feed something like this:
|
Queries are returned as an RSS feed something like this:
|
||||||
|
|
||||||
```rusts
|
```rss
|
||||||
<rss version="1.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
<rss version="1.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||||
<channel>
|
<channel>
|
||||||
<atom:link rel="self" type="application/rss+xml" />
|
<atom:link rel="self" type="application/rss+xml" />
|
||||||
|
@ -65,6 +65,7 @@ Item attributes:
|
||||||
- URLs:
|
- URLs:
|
||||||
- Main URI can either be a magnet URI or a link to a .torrent file: `<enclosure url="http://localhost/" length="0" type="application/x-bittorrent" />`
|
- Main URI can either be a magnet URI or a link to a .torrent file: `<enclosure url="http://localhost/" length="0" type="application/x-bittorrent" />`
|
||||||
- Length is ambiguous, so it will just be 0 (see below)
|
- Length is ambiguous, so it will just be 0 (see below)
|
||||||
|
- for magnet: `application/x-bittorrent;x-scheme-handler/magnet`
|
||||||
- If .torrent URL is provided, use that, if not use the magnet; also put the magnet in `magneturl`
|
- If .torrent URL is provided, use that, if not use the magnet; also put the magnet in `magneturl`
|
||||||
- Rest of available attributes: <https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html?highlight=server#predefined-attributes>
|
- Rest of available attributes: <https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html?highlight=server#predefined-attributes>
|
||||||
|
|
||||||
|
|
392
src/api.rs
392
src/api.rs
|
@ -34,14 +34,12 @@ struct SearchForm {
|
||||||
impl SearchForm {
|
impl SearchForm {
|
||||||
/// Converts it to a SearchParameters object
|
/// Converts it to a SearchParameters object
|
||||||
fn to_parameters(&self, conf: Config) -> InternalSearchParameters {
|
fn to_parameters(&self, conf: Config) -> InternalSearchParameters {
|
||||||
// TODO: Clean up this code - split it into a separate function?
|
|
||||||
let mut categories: Option<Vec<u32>> = None;
|
let mut categories: Option<Vec<u32>> = None;
|
||||||
if !self.cat.is_none() {
|
if !self.cat.is_none() {
|
||||||
// unholy amalgation of code to make the comma-separated list of strings into a vector of integers
|
// unholy amalgation of code to make the comma-separated list of strings into a vector of integers
|
||||||
categories = Some(
|
categories = Some(
|
||||||
self.cat
|
self.cat
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("")
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter_map(|s| s.parse().ok())
|
.filter_map(|s| s.parse().ok())
|
||||||
|
@ -54,7 +52,6 @@ impl SearchForm {
|
||||||
extended_attribute_names = Some(
|
extended_attribute_names = Some(
|
||||||
self.attrs
|
self.attrs
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or("")
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.split(",")
|
.split(",")
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
|
@ -109,10 +106,13 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
||||||
// So this is needed
|
// So this is needed
|
||||||
let conf;
|
let conf;
|
||||||
unsafe {
|
unsafe {
|
||||||
if CONFIG.is_none() {
|
match CONFIG {
|
||||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
Some(ref config) => {
|
||||||
} else {
|
conf = config.clone();
|
||||||
conf = CONFIG.clone().ok_or("").unwrap();
|
}
|
||||||
|
None => {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,14 +122,13 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
||||||
writer.write(XmlEvent::start_element("caps")).unwrap();
|
writer.write(XmlEvent::start_element("caps")).unwrap();
|
||||||
|
|
||||||
// add the server info
|
// 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");
|
let mut element = XmlEvent::start_element("server");
|
||||||
match &conf.caps.server_info {
|
match &conf.caps.server_info {
|
||||||
Some(server_info) => {
|
Some(server_info) => {
|
||||||
// needs to be a vec since if i just `.as_str()` them, they don't live long enough
|
// 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();
|
let server_info_vec: Vec<(&String, &String)> = server_info.iter().collect();
|
||||||
for (key, value) in server_info_vec {
|
for (key, value) in server_info_vec {
|
||||||
element = element.attr(key.as_str(), value.as_str());
|
element = element.attr(key.as_str(), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
|
@ -235,8 +234,7 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api?t=search&<form..>")]
|
#[get("/api?t=search&<form..>")]
|
||||||
/// The search function for the API
|
/// The general search function
|
||||||
// FIXME: VERY incomplete also
|
|
||||||
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
|
||||||
|
@ -254,7 +252,7 @@ pub(crate) fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
||||||
let mut unauthorized = false;
|
let mut unauthorized = false;
|
||||||
match conf.auth {
|
match conf.auth {
|
||||||
Some(auth) => {
|
Some(auth) => {
|
||||||
match parameters.apikey {
|
match parameters.clone().apikey {
|
||||||
Some(apikey) => {
|
Some(apikey) => {
|
||||||
if !auth(apikey).unwrap() {
|
if !auth(apikey).unwrap() {
|
||||||
unauthorized = true;
|
unauthorized = true;
|
||||||
|
@ -273,8 +271,370 @@ pub(crate) fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
||||||
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return status::Custom(
|
let search_parameters: SearchParameters = parameters.to_search_param("search");
|
||||||
Status::NotImplemented,
|
|
||||||
RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
return search_handler(conf, search_parameters);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
#[get("/api?t=tvsearch&<form..>")]
|
||||||
|
/// The TV search function
|
||||||
|
pub(crate) fn tv_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;
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
conf = CONFIG.clone().ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = form.to_parameters(conf.clone());
|
||||||
|
|
||||||
|
let mut unauthorized = false;
|
||||||
|
match conf.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
match parameters.clone().apikey {
|
||||||
|
Some(apikey) => {
|
||||||
|
if !auth(apikey).unwrap() {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// that unwrap_or_else is to return "" if the apikey isn't specified
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unauthorized {
|
||||||
|
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_parameters: SearchParameters = parameters.to_search_param("tv-search");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return status::Custom(
|
||||||
|
* Status::NotImplemented,
|
||||||
|
* RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
return search_handler(conf, search_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api?t=movie&<form..>")]
|
||||||
|
/// The movie search function
|
||||||
|
pub(crate) fn movie_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;
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
conf = CONFIG.clone().ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = form.to_parameters(conf.clone());
|
||||||
|
|
||||||
|
let mut unauthorized = false;
|
||||||
|
match conf.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
match parameters.clone().apikey {
|
||||||
|
Some(apikey) => {
|
||||||
|
if !auth(apikey).unwrap() {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// that unwrap_or_else is to return "" if the apikey isn't specified
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unauthorized {
|
||||||
|
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_parameters: SearchParameters = parameters.to_search_param("movie-search");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return status::Custom(
|
||||||
|
* Status::NotImplemented,
|
||||||
|
* RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
return search_handler(conf, search_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api?t=music&<form..>")]
|
||||||
|
/// The music search function
|
||||||
|
pub(crate) fn music_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;
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
conf = CONFIG.clone().ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = form.to_parameters(conf.clone());
|
||||||
|
|
||||||
|
let mut unauthorized = false;
|
||||||
|
match conf.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
match parameters.clone().apikey {
|
||||||
|
Some(apikey) => {
|
||||||
|
if !auth(apikey).unwrap() {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// that unwrap_or_else is to return "" if the apikey isn't specified
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unauthorized {
|
||||||
|
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_parameters: SearchParameters = parameters.to_search_param("audio-search");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return status::Custom(
|
||||||
|
* Status::NotImplemented,
|
||||||
|
* RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
return search_handler(conf, search_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api?t=book&<form..>")]
|
||||||
|
/// The music search function
|
||||||
|
pub(crate) fn book_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;
|
||||||
|
unsafe {
|
||||||
|
if CONFIG.is_none() {
|
||||||
|
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||||
|
} else {
|
||||||
|
conf = CONFIG.clone().ok_or("").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parameters = form.to_parameters(conf.clone());
|
||||||
|
|
||||||
|
let mut unauthorized = false;
|
||||||
|
match conf.auth {
|
||||||
|
Some(auth) => {
|
||||||
|
match parameters.clone().apikey {
|
||||||
|
Some(apikey) => {
|
||||||
|
if !auth(apikey).unwrap() {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
unauthorized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// that unwrap_or_else is to return "" if the apikey isn't specified
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unauthorized {
|
||||||
|
return status::Custom(Status::Unauthorized, RawXml("401 Unauthorized".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let search_parameters: SearchParameters = parameters.to_search_param("book-search");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* return status::Custom(
|
||||||
|
* Status::NotImplemented,
|
||||||
|
* RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
return search_handler(conf, search_parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_handler(conf: Config, parameters: SearchParameters) -> status::Custom<RawXml<String>> {
|
||||||
|
let buffer = Vec::new();
|
||||||
|
let mut writer = EmitterConfig::new().create_writer(buffer);
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("rss")
|
||||||
|
.attr("version", "1.0")
|
||||||
|
.attr("xmlns:atom", "http://www.w3.org/2005/Atom")
|
||||||
|
.attr("xmlns:torznab", "http://torznab.com/schemas/2015/feed"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writer.write(XmlEvent::start_element("channel")).unwrap();
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("atom:link")
|
||||||
|
.attr("rel", "self")
|
||||||
|
.attr("type", "application/rss+xml"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// add `title`
|
||||||
|
writer.write(XmlEvent::start_element("title")).unwrap();
|
||||||
|
let mut title_provided = false;
|
||||||
|
match conf.caps.server_info {
|
||||||
|
Some(server_info) => {
|
||||||
|
if server_info.contains_key("title") {
|
||||||
|
match server_info.get("title") {
|
||||||
|
Some(title) => {
|
||||||
|
writer.write(XmlEvent::characters(title)).unwrap();
|
||||||
|
title_provided = true;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
if !title_provided {
|
||||||
|
writer
|
||||||
|
.write(XmlEvent::characters("Torznab indexer"))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
|
||||||
|
for item in (conf.search)(parameters).unwrap() {
|
||||||
|
let torrent_file_url = item.torrent_file_url.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
let magnet_uri = item.magnet_uri.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
if torrent_file_url == "" && magnet_uri == "" {
|
||||||
|
panic!("Torrent contains neither a .torrent file URL, not a magnet URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
// start `item`
|
||||||
|
writer.write(XmlEvent::start_element("item")).unwrap();
|
||||||
|
|
||||||
|
// add `title`
|
||||||
|
writer.write(XmlEvent::start_element("title")).unwrap();
|
||||||
|
writer.write(XmlEvent::characters(&item.title)).unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
|
||||||
|
// add `description`
|
||||||
|
writer
|
||||||
|
.write(XmlEvent::start_element("description"))
|
||||||
|
.unwrap();
|
||||||
|
if !item.description.is_none() {
|
||||||
|
writer
|
||||||
|
.write(XmlEvent::characters(&item.description.unwrap_or_default()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
|
||||||
|
// add `size` (torznab attr)
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("torznab:attr")
|
||||||
|
.attr("size", item.size.to_string().as_str()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
|
||||||
|
// add `category`s (torznab attr)
|
||||||
|
for id in item.category_ids {
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("torznab:attr")
|
||||||
|
.attr("name", "category")
|
||||||
|
.attr("value", id.to_string().as_str()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add `link` and `enclosure` (for torrent/magnet uri)
|
||||||
|
// first check if `link` exists in hashmap, and if not, fallback to `torrent_file_url`, then `magnet_uri`
|
||||||
|
writer.write(XmlEvent::start_element("link")).unwrap();
|
||||||
|
let mut link_filled = false; // nesting two layers down of matches, so this is to keep track rather than just doing it in the None
|
||||||
|
match item.other_attributes {
|
||||||
|
Some(ref attributes) => match attributes.get("link") {
|
||||||
|
Some(tmp) => {
|
||||||
|
writer.write(XmlEvent::characters(tmp)).unwrap();
|
||||||
|
link_filled = true;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
},
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !link_filled {
|
||||||
|
match item.torrent_file_url {
|
||||||
|
Some(ref url) => {
|
||||||
|
writer.write(XmlEvent::characters(&url)).unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("enclosure")
|
||||||
|
.attr("url", &url)
|
||||||
|
.attr("length", 0.to_string().as_str())
|
||||||
|
.attr("type", "application/x-bittorrent"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
writer.write(XmlEvent::characters(&magnet_uri)).unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
writer
|
||||||
|
.write(
|
||||||
|
XmlEvent::start_element("enclosure")
|
||||||
|
.attr("url", &magnet_uri)
|
||||||
|
.attr("length", 0.to_string().as_str())
|
||||||
|
.attr("type", "application/x-bittorrent;x-scheme-handler/magnet"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the remaining `other_attributes`
|
||||||
|
match item.other_attributes {
|
||||||
|
Some(ref other_attributes) => {
|
||||||
|
for (key, value) in other_attributes {
|
||||||
|
writer
|
||||||
|
.write(XmlEvent::start_element("torznab::attr").attr(key.as_str(), value))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap();
|
||||||
|
}
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap(); // close `title`
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap(); // close `channel`
|
||||||
|
writer.write(XmlEvent::end_element()).unwrap(); // close `rss`
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
54
src/data.rs
54
src/data.rs
|
@ -4,8 +4,7 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use rocket::FromForm;
|
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
|
pub(crate) type SearchFunc = fn(SearchParameters) -> Result<Vec<Torrent>, String>;
|
||||||
pub(crate) type SearchFunc = fn(String, Vec<String>) -> Result<String, 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
|
||||||
|
@ -37,7 +36,7 @@ pub struct SearchInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Contains subcategories, for use in `Category`
|
/// Contains subcategories, for use in [`Category`]
|
||||||
pub struct Subcategory {
|
pub struct Subcategory {
|
||||||
/// The numeric ID of a subcategory
|
/// The numeric ID of a subcategory
|
||||||
///
|
///
|
||||||
|
@ -48,7 +47,7 @@ pub struct Subcategory {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Contains a category, for use in `Caps` and searches as a query parameter
|
/// Contains a category, for use in [`Caps`] and searches as a query parameter
|
||||||
pub struct Category {
|
pub struct Category {
|
||||||
/// The numeric ID of a category
|
/// The numeric ID of a category
|
||||||
///
|
///
|
||||||
|
@ -61,7 +60,7 @@ pub struct Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Contains a genre, for use in `Caps` and searches as a query parameter
|
/// Contains a genre, for use in [`Caps`] and searches as a query parameter
|
||||||
pub struct Genre {
|
pub struct Genre {
|
||||||
/// The numeric ID of a genre
|
/// The numeric ID of a genre
|
||||||
///
|
///
|
||||||
|
@ -74,7 +73,7 @@ pub struct Genre {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Contains a tag, for use in `Caps` and searches as a query parameter
|
/// Contains a tag, for use in [`Caps`] and searches as a query parameter
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
/// The name of a tag for a torrent
|
/// The name of a tag for a torrent
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -89,7 +88,6 @@ pub struct Tag {
|
||||||
///
|
///
|
||||||
/// 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
|
|
||||||
pub struct Caps {
|
pub struct Caps {
|
||||||
/// The server info, like title - optional
|
/// The server info, like title - optional
|
||||||
///
|
///
|
||||||
|
@ -146,9 +144,12 @@ pub(crate) struct InternalSearchParameters {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InternalSearchParameters {
|
impl InternalSearchParameters {
|
||||||
pub(crate) fn to_search_param(&self, search_type: String) -> SearchParameters {
|
/// Converts InternalSearchParameters to SearchParmaters, i.e. add `search_type`
|
||||||
|
///
|
||||||
|
/// Search types: `search`, `tv-search`, `movie-search`, `audio-search`, `book-search`
|
||||||
|
pub(crate) fn to_search_param(&self, search_type: &str) -> SearchParameters {
|
||||||
return SearchParameters {
|
return SearchParameters {
|
||||||
search_type: search_type,
|
search_type: search_type.to_string(),
|
||||||
q: self.q.clone(),
|
q: self.q.clone(),
|
||||||
apikey: self.apikey.clone(),
|
apikey: self.apikey.clone(),
|
||||||
categories: self.categories.clone(),
|
categories: self.categories.clone(),
|
||||||
|
@ -184,17 +185,28 @@ pub struct SearchParameters {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Holds the info for a torrent
|
/// Holds the info for a torrent
|
||||||
|
///
|
||||||
|
/// Any attributes not listed here are optional, and can be put in `other_attributes`; **however**, the following are recommended:
|
||||||
|
/// - `seeders`
|
||||||
|
/// - `leechers`
|
||||||
|
/// - `peers`
|
||||||
|
/// - `infohash`
|
||||||
|
/// - `link` (link to a webpage; if not specified, will fallback to `torrent_file_url`, then `magnet_uri`)
|
||||||
|
///
|
||||||
|
/// <div class="warning">One of either `torrent_file_url` or `magnet_uri` are required.</div>
|
||||||
pub struct Torrent {
|
pub struct Torrent {
|
||||||
title: String,
|
/// The title of the torrent
|
||||||
description: Option<String>,
|
pub title: String,
|
||||||
size: u64,
|
/// The description of the torrent - optional
|
||||||
categories: Vec<Category>,
|
pub description: Option<String>,
|
||||||
seeders: Option<u32>,
|
/// The size of the torrent, **in bytes**
|
||||||
leechers: Option<u32>,
|
pub size: u64,
|
||||||
peers: Option<u32>,
|
/// A vector of (sub)category IDs
|
||||||
infohash: Option<String>,
|
pub category_ids: Vec<u32>,
|
||||||
link: Option<String>,
|
/// The URL of the `.torrent` file
|
||||||
torrent_file_url: Option<String>,
|
pub torrent_file_url: Option<String>,
|
||||||
magnet_uri: Option<String>,
|
/// The magnet URI o the torrent
|
||||||
other_attributes: Option<HashMap<String, String>>,
|
pub magnet_uri: Option<String>,
|
||||||
|
/// Any other attributes
|
||||||
|
pub other_attributes: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
12
src/dummy.rs
12
src/dummy.rs
|
@ -20,8 +20,16 @@ macro_rules! hashmap {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy_search_func(_a: String, _b: Vec<String>) -> Result<String, String> {
|
fn dummy_search_func(_a: SearchParameters) -> Result<Vec<Torrent>, String> {
|
||||||
return Ok("hi".to_string());
|
return Ok(vec![Torrent {
|
||||||
|
title: "totally normal torrent".to_string(),
|
||||||
|
description: None,
|
||||||
|
size: 9872349573,
|
||||||
|
category_ids: vec![1010],
|
||||||
|
torrent_file_url: Some("http://localhost/totally-normal.torrent".to_string()),
|
||||||
|
magnet_uri: Some("magnet:?xt=urn:btih:blahblahblahdothechachacha".to_string()),
|
||||||
|
other_attributes: None,
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy_auth_func(_a: String) -> Result<bool, String> {
|
fn dummy_auth_func(_a: String) -> Result<bool, String> {
|
||||||
|
|
|
@ -5,4 +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 parsing for `season` and `ep`
|
// TODO: Add parsing for `season` and `ep`
|
||||||
|
|
Loading…
Reference in a new issue