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 |
|
||||
| -------- | ------------------------------------------------------------ | ----------- |
|
||||
| caps | Returns the capabilities of the api. | ✅ |
|
||||
| search | Free text search query. | ❌ |
|
||||
| tvsearch | Search query with tv 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. | ❌ |
|
||||
| book | Search query with book specific query params and filtering. | ❌ |
|
||||
| search | Free text search query. | ✅ |
|
||||
| tvsearch | Search query with tv 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. | ✅ |
|
||||
| book | Search query with book specific query params and filtering. | ✅ |
|
||||
|
||||
<!-- for copy-pasting: ❌ ✅ -->
|
||||
(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:
|
||||
|
||||
```rusts
|
||||
```rss
|
||||
<rss version="1.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||
<channel>
|
||||
<atom:link rel="self" type="application/rss+xml" />
|
||||
|
@ -65,6 +65,7 @@ Item attributes:
|
|||
- 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" />`
|
||||
- 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`
|
||||
- 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 {
|
||||
/// Converts it to a SearchParameters object
|
||||
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;
|
||||
if !self.cat.is_none() {
|
||||
// unholy amalgation of code to make the comma-separated list of strings into a vector of integers
|
||||
categories = Some(
|
||||
self.cat
|
||||
.as_ref()
|
||||
.ok_or("")
|
||||
.unwrap()
|
||||
.split(",")
|
||||
.filter_map(|s| s.parse().ok())
|
||||
|
@ -54,7 +52,6 @@ impl SearchForm {
|
|||
extended_attribute_names = Some(
|
||||
self.attrs
|
||||
.as_ref()
|
||||
.ok_or("")
|
||||
.unwrap()
|
||||
.split(",")
|
||||
.map(|s| s.to_string())
|
||||
|
@ -109,10 +106,13 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
|||
// So this is needed
|
||||
let conf;
|
||||
unsafe {
|
||||
if CONFIG.is_none() {
|
||||
return (*STATUS_CONFIG_NOT_SPECIFIED).clone();
|
||||
} else {
|
||||
conf = CONFIG.clone().ok_or("").unwrap();
|
||||
match CONFIG {
|
||||
Some(ref config) => {
|
||||
conf = config.clone();
|
||||
}
|
||||
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();
|
||||
|
||||
// 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");
|
||||
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());
|
||||
element = element.attr(key.as_str(), value);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
|
@ -235,8 +234,7 @@ pub(crate) fn caps() -> status::Custom<RawXml<String>> {
|
|||
}
|
||||
|
||||
#[get("/api?t=search&<form..>")]
|
||||
/// The search function for the API
|
||||
// FIXME: VERY incomplete also
|
||||
/// The general search function
|
||||
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
|
||||
|
@ -254,7 +252,7 @@ pub(crate) fn search(form: SearchForm) -> status::Custom<RawXml<String>> {
|
|||
let mut unauthorized = false;
|
||||
match conf.auth {
|
||||
Some(auth) => {
|
||||
match parameters.apikey {
|
||||
match parameters.clone().apikey {
|
||||
Some(apikey) => {
|
||||
if !auth(apikey).unwrap() {
|
||||
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::NotImplemented,
|
||||
RawXml("501 Not Implemented: Search function not implemented".to_string()),
|
||||
);
|
||||
let search_parameters: SearchParameters = parameters.to_search_param("search");
|
||||
|
||||
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;
|
||||
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(String, Vec<String>) -> Result<String, String>;
|
||||
pub(crate) type SearchFunc = fn(SearchParameters) -> Result<Vec<Torrent>, String>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// The maximum and defaults for the `limit` parameter in queries
|
||||
|
@ -37,7 +36,7 @@ pub struct SearchInfo {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Contains subcategories, for use in `Category`
|
||||
/// Contains subcategories, for use in [`Category`]
|
||||
pub struct Subcategory {
|
||||
/// The numeric ID of a subcategory
|
||||
///
|
||||
|
@ -48,7 +47,7 @@ pub struct Subcategory {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
/// The numeric ID of a category
|
||||
///
|
||||
|
@ -61,7 +60,7 @@ pub struct Category {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
/// The numeric ID of a genre
|
||||
///
|
||||
|
@ -74,7 +73,7 @@ pub struct Genre {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
/// The name of a tag for a torrent
|
||||
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>
|
||||
///
|
||||
/// TODO: Add a way to partially(?) generate automatically from the Config
|
||||
pub struct Caps {
|
||||
/// The server info, like title - optional
|
||||
///
|
||||
|
@ -146,9 +144,12 @@ pub(crate) struct 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 {
|
||||
search_type: search_type,
|
||||
search_type: search_type.to_string(),
|
||||
q: self.q.clone(),
|
||||
apikey: self.apikey.clone(),
|
||||
categories: self.categories.clone(),
|
||||
|
@ -184,17 +185,28 @@ pub struct SearchParameters {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// 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 {
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
size: u64,
|
||||
categories: Vec<Category>,
|
||||
seeders: Option<u32>,
|
||||
leechers: Option<u32>,
|
||||
peers: Option<u32>,
|
||||
infohash: Option<String>,
|
||||
link: Option<String>,
|
||||
torrent_file_url: Option<String>,
|
||||
magnet_uri: Option<String>,
|
||||
other_attributes: Option<HashMap<String, String>>,
|
||||
/// The title of the torrent
|
||||
pub title: String,
|
||||
/// The description of the torrent - optional
|
||||
pub description: Option<String>,
|
||||
/// The size of the torrent, **in bytes**
|
||||
pub size: u64,
|
||||
/// A vector of (sub)category IDs
|
||||
pub category_ids: Vec<u32>,
|
||||
/// The URL of the `.torrent` file
|
||||
pub torrent_file_url: Option<String>,
|
||||
/// The magnet URI o the torrent
|
||||
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> {
|
||||
return Ok("hi".to_string());
|
||||
fn dummy_search_func(_a: SearchParameters) -> Result<Vec<Torrent>, 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> {
|
||||
|
|
|
@ -5,4 +5,7 @@
|
|||
//! - 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.
|
||||
//! - 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`
|
||||
|
|
Loading…
Reference in a new issue