implement clickable preload files, rename static to public, rename preload config argument to public, rename Storage to Public
|
|
@ -43,12 +43,13 @@ See the [Wiki](https://github.com/YGGverse/btracker/wiki/Screenshots) page
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
btracker --preload=/path/to/aquatic-crawler/preload\
|
btracker --public=/path/to/aquatic-crawler/preload\
|
||||||
--scrape=udp://127.0.0.1:6969\
|
--scrape=udp://127.0.0.1:6969\
|
||||||
--tracker=udp://[302:68d0:f0d5:b88d::fdb]:6969\
|
--tracker=udp://[302:68d0:f0d5:b88d::fdb]:6969\
|
||||||
--tracker=udp://tracker.ygg:6969
|
--tracker=udp://tracker.ygg:6969
|
||||||
```
|
```
|
||||||
* The `--preload` argument specifies the location of the crawled torrents (see [aquatic-crawler](https://github.com/yggverse/aquatic-crawler))
|
* The `--public` argument specifies the location of the crawled torrents (see [aquatic-crawler](https://github.com/yggverse/aquatic-crawler))
|
||||||
|
* make sure this location also contains a copy (or symlink) of the `/public` files from this crate (see the [Rocket deploying specification](https://rocket.rs/guide/v0.5/deploying/) for details)
|
||||||
* The `--scrape` argument is optional and enables statistics for peers, seeders, and leechers
|
* The `--scrape` argument is optional and enables statistics for peers, seeders, and leechers
|
||||||
* it is recommended to use the local address for faster performance
|
* it is recommended to use the local address for faster performance
|
||||||
* this argument supports multiple definitions for both the IPv4 and IPv6 protocols, parsed from the URL value
|
* this argument supports multiple definitions for both the IPv4 and IPv6 protocols, parsed from the URL value
|
||||||
|
|
|
||||||
3
public/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/*
|
||||||
|
!.gitignore
|
||||||
|
!/theme
|
||||||
|
|
@ -45,6 +45,7 @@ h1, h2, h3, h4, h5 {
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|
@ -108,7 +109,7 @@ main > a {
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
opacity: .96;
|
opacity: .96;
|
||||||
padding: 8px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* item row */
|
/* item row */
|
||||||
|
|
@ -129,7 +130,7 @@ main > div > div {
|
||||||
border-top: 1px solid var(--separator);
|
border-top: 1px solid var(--separator);
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: 16px;
|
padding: 16px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
main > div > div > ul {
|
main > div > div > ul {
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
|
|
@ -8,9 +8,13 @@ use url::Url;
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Path to the [aquatic-crawler](https://github.com/yggverse/aquatic-crawler) preload files
|
/// Path to the `public` directory
|
||||||
|
///
|
||||||
|
/// This location must contain:
|
||||||
|
/// * the default or custom `/public/*` files (see the [Rocket deploying specification](https://rocket.rs/guide/v0.5/deploying/))
|
||||||
|
/// * torrents with files collected by the [aquatic-crawler](https://github.com/yggverse/aquatic-crawler)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub preload: PathBuf,
|
pub public: PathBuf,
|
||||||
|
|
||||||
/// Server name
|
/// Server name
|
||||||
#[arg(long, default_value_t = String::from("βtracker"))]
|
#[arg(long, default_value_t = String::from("βtracker"))]
|
||||||
|
|
@ -34,15 +38,11 @@ pub struct Config {
|
||||||
#[arg(long, default_value_t = String::from("%d/%m/%Y %H:%M"))]
|
#[arg(long, default_value_t = String::from("%d/%m/%Y %H:%M"))]
|
||||||
pub format_time: String,
|
pub format_time: String,
|
||||||
|
|
||||||
/// Path to the framework assets
|
|
||||||
#[arg(long, default_value_t = String::from("./static"))]
|
|
||||||
pub statics: String,
|
|
||||||
|
|
||||||
/// Default listing limit
|
/// Default listing limit
|
||||||
#[arg(long, default_value_t = 20)]
|
#[arg(long, default_value_t = 20)]
|
||||||
pub list_limit: usize,
|
pub list_limit: usize,
|
||||||
|
|
||||||
/// Default capacity (estimated torrents in the `preload` directory)
|
/// Default capacity (estimated torrents in the `public` directory)
|
||||||
#[arg(long, default_value_t = 1000)]
|
#[arg(long, default_value_t = 1000)]
|
||||||
pub capacity: usize,
|
pub capacity: usize,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ impl Feed {
|
||||||
pub fn push(&self, buffer: &mut String, torrent: Torrent) {
|
pub fn push(&self, buffer: &mut String, torrent: Torrent) {
|
||||||
buffer.push_str(&format!(
|
buffer.push_str(&format!(
|
||||||
"<item><guid>{}</guid><title>{}</title><link>{}</link>",
|
"<item><guid>{}</guid><title>{}</title><link>{}</link>",
|
||||||
&torrent.info_hash,
|
torrent.info_hash,
|
||||||
escape(
|
escape(
|
||||||
torrent
|
torrent
|
||||||
.name
|
.name
|
||||||
|
|
|
||||||
52
src/main.rs
|
|
@ -4,14 +4,15 @@ extern crate rocket;
|
||||||
mod config;
|
mod config;
|
||||||
mod feed;
|
mod feed;
|
||||||
mod meta;
|
mod meta;
|
||||||
|
mod public;
|
||||||
mod scraper;
|
mod scraper;
|
||||||
mod storage;
|
|
||||||
mod torrent;
|
mod torrent;
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use feed::Feed;
|
use feed::Feed;
|
||||||
use meta::Meta;
|
use meta::Meta;
|
||||||
use plurify::Plurify;
|
use plurify::Plurify;
|
||||||
|
use public::{Order, Public, Sort};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
State,
|
State,
|
||||||
http::Status,
|
http::Status,
|
||||||
|
|
@ -21,14 +22,13 @@ use rocket::{
|
||||||
use rocket_dyn_templates::{Template, context};
|
use rocket_dyn_templates::{Template, context};
|
||||||
use scraper::{Scrape, Scraper};
|
use scraper::{Scrape, Scraper};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use storage::{Order, Sort, Storage};
|
|
||||||
use torrent::Torrent;
|
use torrent::Torrent;
|
||||||
|
|
||||||
#[get("/?<page>")]
|
#[get("/?<page>")]
|
||||||
fn index(
|
fn index(
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
scraper: &State<Scraper>,
|
scraper: &State<Scraper>,
|
||||||
storage: &State<Storage>,
|
public: &State<Public>,
|
||||||
meta: &State<Meta>,
|
meta: &State<Meta>,
|
||||||
) -> Result<Template, Custom<String>> {
|
) -> Result<Template, Custom<String>> {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -42,14 +42,14 @@ fn index(
|
||||||
size: String,
|
size: String,
|
||||||
torrent: Torrent,
|
torrent: Torrent,
|
||||||
}
|
}
|
||||||
let (total, torrents) = storage
|
let (total, torrents) = public
|
||||||
.torrents(
|
.torrents(
|
||||||
Some((Sort::Modified, Order::Desc)),
|
Some((Sort::Modified, Order::Desc)),
|
||||||
page.map(|p| if p > 0 { p - 1 } else { p } * storage.default_limit),
|
page.map(|p| if p > 0 { p - 1 } else { p } * public.default_limit),
|
||||||
Some(storage.default_limit),
|
Some(public.default_limit),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Torrents storage read error: `{e}`");
|
error!("Torrents public storage read error: `{e}`");
|
||||||
Custom(Status::InternalServerError, E.to_string())
|
Custom(Status::InternalServerError, E.to_string())
|
||||||
})?;
|
})?;
|
||||||
Ok(Template::render(
|
Ok(Template::render(
|
||||||
|
|
@ -57,11 +57,11 @@ fn index(
|
||||||
context! {
|
context! {
|
||||||
meta: meta.inner(),
|
meta: meta.inner(),
|
||||||
back: page.map(|p| uri!(index(if p > 2 { Some(p - 1) } else { None }))),
|
back: page.map(|p| uri!(index(if p > 2 { Some(p - 1) } else { None }))),
|
||||||
next: if page.unwrap_or(1) * storage.default_limit >= total { None }
|
next: if page.unwrap_or(1) * public.default_limit >= total { None }
|
||||||
else { Some(uri!(index(Some(page.map_or(2, |p| p + 1))))) },
|
else { Some(uri!(index(Some(page.map_or(2, |p| p + 1))))) },
|
||||||
rows: torrents
|
rows: torrents
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|t| match Torrent::from_storage(&t.bytes, t.time) {
|
.filter_map(|t| match Torrent::from_public(&t.bytes, t.time) {
|
||||||
Ok(torrent) => Some(R {
|
Ok(torrent) => Some(R {
|
||||||
created: torrent.creation_date.map(|t| t.format(&meta.format_time).to_string()),
|
created: torrent.creation_date.map(|t| t.format(&meta.format_time).to_string()),
|
||||||
files: torrent.files(),
|
files: torrent.files(),
|
||||||
|
|
@ -80,7 +80,7 @@ fn index(
|
||||||
pagination_totals: format!(
|
pagination_totals: format!(
|
||||||
"Page {} / {} ({total} {} total)",
|
"Page {} / {} ({total} {} total)",
|
||||||
page.unwrap_or(1),
|
page.unwrap_or(1),
|
||||||
(total as f64 / storage.default_limit as f64).ceil(),
|
(total as f64 / public.default_limit as f64).ceil(),
|
||||||
total.plurify(&["torrent", "torrents", "torrents"])
|
total.plurify(&["torrent", "torrents", "torrents"])
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -90,11 +90,11 @@ fn index(
|
||||||
#[get("/<info_hash>")]
|
#[get("/<info_hash>")]
|
||||||
fn info(
|
fn info(
|
||||||
info_hash: &str,
|
info_hash: &str,
|
||||||
storage: &State<Storage>,
|
public: &State<Public>,
|
||||||
scraper: &State<Scraper>,
|
scraper: &State<Scraper>,
|
||||||
meta: &State<Meta>,
|
meta: &State<Meta>,
|
||||||
) -> Result<Template, Custom<String>> {
|
) -> Result<Template, Custom<String>> {
|
||||||
match storage.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|e| {
|
match public.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|e| {
|
||||||
warn!("Torrent info-hash parse error: `{e}`");
|
warn!("Torrent info-hash parse error: `{e}`");
|
||||||
Custom(Status::BadRequest, Status::BadRequest.to_string())
|
Custom(Status::BadRequest, Status::BadRequest.to_string())
|
||||||
})?) {
|
})?) {
|
||||||
|
|
@ -102,10 +102,11 @@ fn info(
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
struct F {
|
struct F {
|
||||||
|
href: Option<String>,
|
||||||
path: String,
|
path: String,
|
||||||
size: String,
|
size: String,
|
||||||
}
|
}
|
||||||
let torrent = Torrent::from_storage(&t.bytes, t.time).map_err(|e| {
|
let torrent = Torrent::from_public(&t.bytes, t.time).map_err(|e| {
|
||||||
error!("Torrent parse error: `{e}`");
|
error!("Torrent parse error: `{e}`");
|
||||||
Custom(Status::InternalServerError, E.to_string())
|
Custom(Status::InternalServerError, E.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -117,15 +118,19 @@ fn info(
|
||||||
files_total: torrent.files(),
|
files_total: torrent.files(),
|
||||||
files_list: torrent.files.as_ref().map(|f| {
|
files_list: torrent.files.as_ref().map(|f| {
|
||||||
f.iter()
|
f.iter()
|
||||||
.map(|f| F {
|
.map(|f| {
|
||||||
path: f.path(),
|
let p = f.path();
|
||||||
|
F {
|
||||||
|
href: public.href(&torrent.info_hash, &p),
|
||||||
|
path: p,
|
||||||
size: f.size(),
|
size: f.size(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<F>>()
|
.collect::<Vec<F>>()
|
||||||
}),
|
}),
|
||||||
indexed: torrent.time.format(&meta.format_time).to_string(),
|
indexed: torrent.time.format(&meta.format_time).to_string(),
|
||||||
magnet: torrent.magnet(meta.trackers.as_ref()),
|
magnet: torrent.magnet(meta.trackers.as_ref()),
|
||||||
scrape: scraper.scrape(info_hash),
|
scrape: scraper.scrape(&torrent.info_hash),
|
||||||
size: torrent.size(),
|
size: torrent.size(),
|
||||||
torrent
|
torrent
|
||||||
},
|
},
|
||||||
|
|
@ -136,23 +141,23 @@ fn info(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/rss")]
|
#[get("/rss")]
|
||||||
fn rss(feed: &State<Feed>, storage: &State<Storage>) -> Result<RawXml<String>, Custom<String>> {
|
fn rss(feed: &State<Feed>, public: &State<Public>) -> Result<RawXml<String>, Custom<String>> {
|
||||||
let mut b = feed.transaction(1024); // @TODO
|
let mut b = feed.transaction(1024); // @TODO
|
||||||
for t in storage
|
for t in public
|
||||||
.torrents(
|
.torrents(
|
||||||
Some((Sort::Modified, Order::Desc)),
|
Some((Sort::Modified, Order::Desc)),
|
||||||
None,
|
None,
|
||||||
Some(storage.default_limit),
|
Some(public.default_limit),
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Torrent storage read error: `{e}`");
|
error!("Torrent public storage read error: `{e}`");
|
||||||
Custom(Status::InternalServerError, E.to_string())
|
Custom(Status::InternalServerError, E.to_string())
|
||||||
})?
|
})?
|
||||||
.1
|
.1
|
||||||
{
|
{
|
||||||
feed.push(
|
feed.push(
|
||||||
&mut b,
|
&mut b,
|
||||||
Torrent::from_storage(&t.bytes, t.time).map_err(|e| {
|
Torrent::from_public(&t.bytes, t.time).map_err(|e| {
|
||||||
error!("Torrent parse error: `{e}`");
|
error!("Torrent parse error: `{e}`");
|
||||||
Custom(Status::InternalServerError, E.to_string())
|
Custom(Status::InternalServerError, E.to_string())
|
||||||
})?,
|
})?,
|
||||||
|
|
@ -199,7 +204,6 @@ fn rocket() -> _ {
|
||||||
})
|
})
|
||||||
.map(|a| (config.udp, a)),
|
.map(|a| (config.udp, a)),
|
||||||
);
|
);
|
||||||
let storage = Storage::init(config.preload, config.list_limit, config.capacity).unwrap();
|
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
.configure(rocket::Config {
|
.configure(rocket::Config {
|
||||||
|
|
@ -213,7 +217,7 @@ fn rocket() -> _ {
|
||||||
})
|
})
|
||||||
.manage(feed)
|
.manage(feed)
|
||||||
.manage(scraper)
|
.manage(scraper)
|
||||||
.manage(storage)
|
.manage(Public::init(config.public.clone(), config.list_limit, config.capacity).unwrap())
|
||||||
.manage(Meta {
|
.manage(Meta {
|
||||||
canonical: config.canonical_url,
|
canonical: config.canonical_url,
|
||||||
description: config.description,
|
description: config.description,
|
||||||
|
|
@ -222,7 +226,7 @@ fn rocket() -> _ {
|
||||||
trackers: config.tracker,
|
trackers: config.tracker,
|
||||||
version: env!("CARGO_PKG_VERSION").into(),
|
version: env!("CARGO_PKG_VERSION").into(),
|
||||||
})
|
})
|
||||||
.mount("/", rocket::fs::FileServer::from(config.statics))
|
.mount("/", rocket::fs::FileServer::from(config.public))
|
||||||
.mount("/", routes![index, info, rss])
|
.mount("/", routes![index, info, rss])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,13 @@ pub struct Torrent {
|
||||||
pub time: DateTime<Utc>,
|
pub time: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Storage {
|
pub struct Public {
|
||||||
pub default_limit: usize,
|
pub default_limit: usize,
|
||||||
default_capacity: usize,
|
default_capacity: usize,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Public {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
|
|
@ -40,7 +40,7 @@ impl Storage {
|
||||||
default_capacity: usize,
|
default_capacity: usize,
|
||||||
) -> Result<Self, String> {
|
) -> Result<Self, String> {
|
||||||
if !root.is_dir() {
|
if !root.is_dir() {
|
||||||
return Err("Storage root is not directory".into());
|
return Err("Public root is not directory".into());
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
default_limit,
|
default_limit,
|
||||||
|
|
@ -88,6 +88,21 @@ impl Storage {
|
||||||
Ok((t, b))
|
Ok((t, b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn href(&self, info_hash: &str, path: &str) -> Option<String> {
|
||||||
|
let mut relative = PathBuf::from(info_hash);
|
||||||
|
relative.push(path);
|
||||||
|
|
||||||
|
let mut absolute = PathBuf::from(&self.root);
|
||||||
|
absolute.push(&relative);
|
||||||
|
|
||||||
|
let c = absolute.canonicalize().ok()?;
|
||||||
|
if c.starts_with(&self.root) && c.exists() {
|
||||||
|
Some(relative.to_string_lossy().into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
fn files(&self, sort_order: Option<(Sort, Order)>) -> Result<Vec<DirEntry>, Error> {
|
fn files(&self, sort_order: Option<(Sort, Order)>) -> Result<Vec<DirEntry>, Error> {
|
||||||
|
|
@ -2,7 +2,7 @@ mod file;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use file::File;
|
use file::File;
|
||||||
use librqbit_core::{torrent_metainfo, torrent_metainfo::TorrentMetaV1Owned};
|
use librqbit_core::torrent_metainfo::{self, TorrentMetaV1Owned};
|
||||||
use rocket::serde::Serialize;
|
use rocket::serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
|
@ -25,7 +25,7 @@ pub struct Torrent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Torrent {
|
impl Torrent {
|
||||||
pub fn from_storage(bytes: &[u8], time: DateTime<Utc>) -> Result<Self, String> {
|
pub fn from_public(bytes: &[u8], time: DateTime<Utc>) -> Result<Self, String> {
|
||||||
let i: TorrentMetaV1Owned =
|
let i: TorrentMetaV1Owned =
|
||||||
torrent_metainfo::torrent_from_bytes(bytes).map_err(|e| e.to_string())?;
|
torrent_metainfo::torrent_from_bytes(bytes).map_err(|e| e.to_string())?;
|
||||||
Ok(Torrent {
|
Ok(Torrent {
|
||||||
|
|
@ -84,11 +84,7 @@ impl Torrent {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn magnet(&self, trackers: Option<&Vec<url::Url>>) -> String {
|
pub fn magnet(&self, trackers: Option<&Vec<url::Url>>) -> String {
|
||||||
let mut b = if self.info_hash.len() == 40 {
|
let mut b = format!("magnet:?xt=urn:btih:{}", self.info_hash);
|
||||||
format!("magnet:?xt=urn:btih:{}", self.info_hash)
|
|
||||||
} else {
|
|
||||||
todo!("info-hash v2 yet not supported") // librqbit_core::hash_id::Id
|
|
||||||
};
|
|
||||||
if let Some(t) = trackers {
|
if let Some(t) = trackers {
|
||||||
for tracker in t {
|
for tracker in t {
|
||||||
b.push_str("&tr=");
|
b.push_str("&tr=");
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,13 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for file in files_list %}
|
{% for file in files_list %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ file.path }}</td>
|
<td>
|
||||||
|
{% if file.href %}
|
||||||
|
<a href="{{ file.href }}">{{ file.path }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ file.path }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ file.size }}</td>
|
<td>{{ file.size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||