diff --git a/README.md b/README.md index 955c843..ab6d988 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,23 @@ # Torznab Toolkit -A safe, multi-threaded toolkit for adding Torznab APIs to programs. +A safe, multi-threaded toolkit for adding Torznab APIs to programs. You just focus on the indexer itself, we abstract away the hell that is the Torznab API. + +Just fill in your own relevant functions and config, and torznab-toolkit will run the API for you + +```rs +use torznab_toolkit; +let config: torznab_toolkit::config::Config = /* config goes here */ + +torznab_toolkit::run(config); +``` + +The environment variables `ROCKET_ADDRESS` and `ROCKET_PORT` specify the address and port it will run on; these currently cannot be configured any other way. See the [relevant docs](https://rocket.rs/guide/v0.5/deploying/) for details. + +--- + +This program is brought to you by: metaphorical *and* literal truckloads of structs! + +Note: I wrote the line above when I was tired. Don't ask me what *literal* truckloads of structs means, I don't know either. ## Functionality @@ -16,6 +33,11 @@ A safe, multi-threaded toolkit for adding Torznab APIs to programs. (copied from [torznab.github.io](https://torznab.github.io/spec-1.3-draft/torznab/Specification-v1.3.html)) +## Limitations + +- Currently this does not allow for returning errors from the program using the library, such as API limits for an account. +- Currently this does not allow for requiring authentication for `caps`; it's against spec (not that that's worth much), but common and perfectly fine to do. + ## Notes -Thanks to [torznab.github.io](https://torznab.github.io/spec-1.3-draft/index.html), as it's my primary reference for this. +Thanks to [torznab.github.io](https://torznab.github.io/spec-1.3-draft/index.html), as it's my primary reference for this; NZBDrone's [Implementing a Torznab indexer](https://nzbdrone.readthedocs.io/Implementing-a-Torznab-indexer/) was also rather helpful. diff --git a/dev-notes.md b/dev-notes.md index dd4444a..aabbb13 100644 --- a/dev-notes.md +++ b/dev-notes.md @@ -4,19 +4,82 @@ - - +- - for testing: --- -```rs -struct TorznabToolkitConfig { - auth_func: auth, - search_func: search, - whateverotherfunc: otherfunc, - port: 5309 -} +example usage: -fn launch() -> Result { - rocket::build().mount("/", routes![tt::search, tt:otherfunc]) +```rs +let config = /* config goes here */ + +fn main() -> Result { + run(config); } ``` + +## Torznab spec + +### Query results + +Queries are returned as an RSS feed something like this: + +```rss + + + + Prowlarr + + Item title + + http://localhost:9999/ + + + + + + + + +``` + +Item attributes: + +- RSS: + - `title`: title of the thing; can maybe be empty? unsure - torznab-toolkit will treat this as required, and set description to be empty + - `description`: description; can just be empty + - `link`: a link + - Preferably specified, if not then fallback to .torrent link, then magnet url + - at least title *or* description is required; link is recommended for compatibility with some non-compliant software, like headphones (see [bitmagnet issue 349](https://github.com/bitmagnet-io/bitmagnet/issues/349)) +- Torznab + - Required: + - `size`: Size in bytes + - `category`: (Sub)category id; can occur multiple times for several categories + - Recommended (by me, based off my own thoughts and NZBDrone's [Implementing a Torznab indexer](https://nzbdrone.readthedocs.io/Implementing-a-Torznab-indexer/) page): + - `seeders`: Number of seeders + - `leechers`: Number of leechers + - `peers`: Number of peers + - `infohash`: Torrent infohash + - `magneturl`: The magnet URI for the torrent + - URLs: + - Main URI can either be a magnet URI or a link to a .torrent file: `` + - Length is ambiguous, so it will just be 0 (see below) + - If .torrent URL is provided, use that, if not use the magnet; also put the magnet in `magneturl` + - Rest of available attributes: + +--- + +## Non-compliance and errors in the Torznab spec + +- Prowlarr's built-in indexer for sites it scrapes (like 1337x) does not respect `limit` +- Negative `limit`: The spec says that if `limit` is negative, then to return `201 - Incorrect parameter`; but 201 is Created, and there's no Incorrect Parameter HTTP status code (my best guess is it should be 400?). Additionally, other indexers just ignore the limit if it's negative, so in that case, torznab-toolkit will just set the limit to the maximum to match the behavior of other widely-used indexers. + +## Ambiguous behavior + +- Due to ambiguous behavior, torznab-toolkit will set `length` to 0 + +> The correct definition of the enclosure length attribute should have been the size of the .torrent file, however newznab defined it as the length of the usenet download rather than the .nzb size. +> Therefore the length attribute can be either 0, or the .torrent/.nzb file size, or the actual media size. Given this ambiguity, services should instead provide the size extended attribute with the size of the media. (eg. ) + +(from [Torznab spec - Torznab Torrent Support](https://torznab.github.io/spec-1.3-draft/revisions/1.0-Torznab-Torrent-Support.html])) diff --git a/src/api.rs b/src/api.rs index 62b6cc1..a1856d6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -8,6 +8,7 @@ use rocket::http::Status; use rocket::response::status; use rocket::FromForm; use rocket::{get, response::content::RawXml}; +use std::collections::HashMap; use xml::writer::{EmitterConfig, XmlEvent}; #[derive(Debug, Clone, PartialEq, Eq, FromForm)] @@ -34,6 +35,7 @@ struct SearchForm { } impl SearchForm { + /// Converts it to a SearchParameters object fn to_parameters(&self, conf: Config) -> SearchParameters { // TODO: Clean up this code - split it into a separate function? let mut categories: Option> = None; @@ -131,6 +133,7 @@ pub(crate) fn caps() -> status::Custom> { #[get("/api?t=search&")] /// The search function for the API +// FIXME: VERY incomplete also 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 @@ -151,10 +154,7 @@ pub(crate) fn search(form: SearchForm) -> status::Custom> { match parameters.apikey { Some(apikey) => { if !auth(apikey).unwrap() { - return status::Custom( - Status::Unauthorized, - RawXml("401 Unauthorized".to_string()), - ); + unauthorized = true; } } None => { diff --git a/src/data.rs b/src/data.rs index 7606602..177f04d 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,5 +1,7 @@ //! Contains tons of structs used by the library +use std::collections::HashMap; + 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 @@ -97,17 +99,23 @@ pub struct Tag { #[derive(Debug, Clone, PartialEq, Eq)] /// 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. +///
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 501. /// /// 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 { - pub server_info: ServerInfo, + /// The server info, like title - optional + pub server_info: Option, + /// The max and default number of items to be returned by queries - see [`Limits`] pub limits: Limits, + /// Info about each type of search pub searching: Vec, + /// What categories the server has - see [`Category`] pub categories: Vec, + /// What genres the server has - see [`Genre`] (optional) pub genres: Option>, + /// What torrents can be tagged with - see [`Tag`] (optional) pub tags: Option>, } @@ -116,12 +124,19 @@ pub struct Caps { /// The search function (`/api?t=search`) and capabilities (`/api?t=caps` - struct [`Caps`]) are required /// Everything else is optional pub struct Config { + /// The function to use for a free text search pub search: SearchFunc, + /// The auth function - if not specified, then no authorization is needed. pub auth: Option, + /// The capabilities of the indexer - see [`Caps`] pub caps: Caps, + /// The function to use for a tv search pub tvsearch: Option, + /// The function to use for a movie search pub movie: Option, + /// The function to use for a music search pub music: Option, + /// The function to use for a book search pub book: Option, } @@ -142,3 +157,20 @@ pub(crate) struct SearchParameters { /// The maximum number of items to return - also limited to whatever `limits` is in [`Caps`] pub(crate) limit: u32, } + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Holds the info for a torrent +pub struct Torrent { + title: String, + description: Option, + size: u64, + categories: Vec, + seeders: Option, + leechers: Option, + peers: Option, + infohash: Option, + link: Option, + torrent_file_url: Option, + magnet_uri: Option, + other_attributes: Option>, +} diff --git a/src/dummy.rs b/src/dummy.rs index 84f201c..4b88fcb 100644 --- a/src/dummy.rs +++ b/src/dummy.rs @@ -48,12 +48,12 @@ pub(crate) fn create_empty_config() -> Config { search: dummy_search_func, auth: Some(dummy_auth_func), caps: Caps { - server_info: ServerInfo { + server_info: Some(ServerInfo { title: Some("Test Torznab server".to_string()), email: Some("test@example.com".to_string()), image: None, version: Some("1.0".to_string()), - }, + }), limits: Limits { max: 100, default: 20, diff --git a/src/lib.rs b/src/lib.rs index b8c75ba..e303197 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,5 @@ #![warn(missing_docs)] -//! A toolkit for adding Torznab APIs to programs. -//! -//! Just fill in your own relevant functions and config, and -//! torznab-toolkit will run the API for you -//! -//! ```rs -//! use torznab_toolkit; -//! let config: torznab_toolkit::config::Config = -//! -//! ``` -//! -//! The environment variables `ROCKET_ADDRESS` and `ROCKET_PORT` specify the address and port it will run on; these currently cannot be configured any other way. See the [relevant docs](https://rocket.rs/guide/v0.5/deploying/) for details. -//! -//! --- -//! -//! This program is brought to you by: metaphorical *and* literal truckloads of structs! -//! -//! Note: I wrote the line above when I was tired. Don't ask me what *literal* truckloads of structs means, I don't know either. - +#![doc = include_str!("../README.md")] pub mod api; pub mod data; mod dummy; @@ -42,3 +24,6 @@ pub async fn run(conf: data::Config) -> Result { } } } + +/// Notes regarding the usage of torznab-toolkit and how it implements the Torznab API. +pub mod notes; diff --git a/src/notes/implementation.rs b/src/notes/implementation.rs new file mode 100644 index 0000000..695b913 --- /dev/null +++ b/src/notes/implementation.rs @@ -0,0 +1,6 @@ +//! Notes regarding the implementation of Torznab +//! +//! - Because the behavior of `length` is ambiguous, torznab-toolkit just sets it to 0; the size is just specified by the `size` attribute +//! - See [here](https://torznab.github.io/spec-1.3-draft/revisions/1.0-Torznab-Torrent-Support.html) for details +//! - Many indexers do not have the appropriate behavior according to the spec when `limit` is negative, and that behavior doesn't even make sense; instead, it follows the behavior of other indexers, and just ignores `limit` if it's negative. +//! - If a link isn't specified for a Torrent diff --git a/src/notes/mod.rs b/src/notes/mod.rs new file mode 100644 index 0000000..08e6406 --- /dev/null +++ b/src/notes/mod.rs @@ -0,0 +1 @@ +pub mod usage; diff --git a/src/notes/usage.rs b/src/notes/usage.rs new file mode 100644 index 0000000..239cb90 --- /dev/null +++ b/src/notes/usage.rs @@ -0,0 +1,8 @@ +//! Notes regarding the usage of torznab-tooolkit +//! +//! --- +//! +//! - 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 parsing for `season` and `ep`