init info page features

This commit is contained in:
yggverse 2025-08-09 06:45:43 +03:00
parent c5a0684466
commit 2fc9535710
7 changed files with 121 additions and 47 deletions

30
src/format.rs Normal file
View file

@ -0,0 +1,30 @@
use crate::{Meta, Scrape, Scraper, Torrent};
use rocket::{State, serde::Serialize};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Format {
pub created: Option<String>,
pub files: String,
pub indexed: String,
pub magnet: String,
pub scrape: Option<Scrape>,
pub size: String,
pub torrent: Torrent,
}
impl Format {
pub fn from_torrent(torrent: Torrent, scraper: &State<Scraper>, meta: &State<Meta>) -> Self {
Self {
created: torrent
.creation_date
.map(|t| t.format(&meta.format_time).to_string()),
indexed: torrent.time.format(&meta.format_time).to_string(),
magnet: torrent.magnet(meta.trackers.as_ref()),
scrape: scraper.scrape(&torrent.info_hash),
size: torrent.size(),
files: torrent.files(),
torrent,
}
}
}

View file

@ -3,36 +3,27 @@ extern crate rocket;
mod config; mod config;
mod feed; mod feed;
mod format;
mod meta;
mod scraper; mod scraper;
mod storage; mod storage;
mod torrent; mod torrent;
use config::Config; use config::Config;
use feed::Feed; use feed::Feed;
use format::Format;
use meta::Meta;
use plurify::Plurify; use plurify::Plurify;
use rocket::{ use rocket::{
State, State,
http::Status, http::Status,
response::{content::RawXml, status::Custom}, response::{content::RawXml, status::Custom},
serde::Serialize,
}; };
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 storage::{Order, Sort, Storage}; use storage::{Order, Sort, Storage};
use torrent::Torrent; use torrent::Torrent;
use url::Url;
#[derive(Clone, Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Meta {
pub canonical: Option<Url>,
pub description: Option<String>,
pub format_time: String,
pub title: String,
/// * use vector to keep the order from the arguments list
pub trackers: Option<Vec<Url>>,
pub version: String,
}
#[get("/?<page>")] #[get("/?<page>")]
fn index( fn index(
@ -41,18 +32,6 @@ fn index(
storage: &State<Storage>, storage: &State<Storage>,
meta: &State<Meta>, meta: &State<Meta>,
) -> Result<Template, Custom<String>> { ) -> Result<Template, Custom<String>> {
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Row {
created: Option<String>,
files: String,
indexed: String,
magnet: String,
scrape: Option<Scrape>,
size: String,
torrent: Torrent,
}
let (total, torrents) = storage let (total, torrents) = storage
.torrents( .torrents(
Some((Sort::Modified, Order::Desc)), Some((Sort::Modified, Order::Desc)),
@ -74,23 +53,13 @@ fn index(
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_storage(&t.bytes, t.time) {
Ok(torrent) => Some(Row { Ok(torrent) => Some(Format::from_torrent(torrent, scraper, meta)),
created: torrent
.creation_date
.map(|t| t.format(&meta.format_time).to_string()),
indexed: torrent.time.format(&meta.format_time).to_string(),
magnet: torrent.magnet(meta.trackers.as_ref()),
scrape: scraper.scrape(&torrent.info_hash),
size: torrent.size(),
files: torrent.files(),
torrent,
}),
Err(e) => { Err(e) => {
error!("Torrent storage read error: `{e}`"); error!("Torrent storage read error: `{e}`");
None None
} }
}) })
.collect::<Vec<Row>>(), .collect::<Vec<Format>>(),
pagination_totals: format!( pagination_totals: format!(
"Page {} / {} ({total} {} total)", "Page {} / {} ({total} {} total)",
page.unwrap_or(1), page.unwrap_or(1),
@ -101,6 +70,34 @@ fn index(
)) ))
} }
#[get("/<info_hash>")]
fn info(
info_hash: &str,
storage: &State<Storage>,
scraper: &State<Scraper>,
meta: &State<Meta>,
) -> Result<Template, Custom<String>> {
match storage.torrent(librqbit_core::Id20::from_str(info_hash).map_err(|e| {
warn!("Torrent info-hash parse error: `{e}`");
Custom(Status::BadRequest, Status::BadRequest.to_string())
})?) {
Some(t) => Ok(Template::render(
"info",
context! {
meta: meta.inner(),
torrent: Format::from_torrent(
Torrent::from_storage(&t.bytes, t.time).map_err(|e| {
error!("Torrent parse error: `{e}`");
Custom(Status::InternalServerError, E.to_string())
})?, scraper, meta
),
info_hash
},
)),
None => Err(Custom(Status::NotFound, E.to_string())),
}
}
#[get("/rss")] #[get("/rss")]
fn rss(feed: &State<Feed>, storage: &State<Storage>) -> Result<RawXml<String>, Custom<String>> { fn rss(feed: &State<Feed>, storage: &State<Storage>) -> Result<RawXml<String>, Custom<String>> {
let mut b = feed.transaction(1024); // @TODO let mut b = feed.transaction(1024); // @TODO
@ -189,7 +186,7 @@ fn rocket() -> _ {
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.statics))
.mount("/", routes![index, rss]) .mount("/", routes![index, info, rss])
} }
/// Public placeholder text for the `Status::InternalServerError` /// Public placeholder text for the `Status::InternalServerError`

14
src/meta.rs Normal file
View file

@ -0,0 +1,14 @@
use rocket::serde::Serialize;
use url::Url;
#[derive(Clone, Debug, Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Meta {
pub canonical: Option<Url>,
pub description: Option<String>,
pub format_time: String,
pub title: String,
/// * use vector to keep the order from the arguments list
pub trackers: Option<Vec<Url>>,
pub version: String,
}

View file

@ -5,6 +5,8 @@ use std::{
path::PathBuf, path::PathBuf,
}; };
const EXTENSION: &str = "torrent";
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum Sort { pub enum Sort {
#[default] #[default]
@ -49,6 +51,15 @@ impl Storage {
// Getters // Getters
pub fn torrent(&self, info_hash: librqbit_core::Id20) -> Option<Torrent> {
let mut p = PathBuf::from(&self.root);
p.push(format!("{}.{EXTENSION}", info_hash.as_string()));
Some(Torrent {
bytes: fs::read(&p).ok()?,
time: p.metadata().ok()?.modified().ok()?.into(),
})
}
pub fn torrents( pub fn torrents(
&self, &self,
sort_order: Option<(Sort, Order)>, sort_order: Option<(Sort, Order)>,
@ -66,7 +77,7 @@ impl Storage {
.filter(|f| { .filter(|f| {
f.path() f.path()
.extension() .extension()
.is_some_and(|e| !e.is_empty() && e.to_string_lossy() == "torrent") .is_some_and(|e| !e.is_empty() && e.to_string_lossy() == EXTENSION)
}) })
{ {
b.push(Torrent { b.push(Torrent {

View file

@ -43,12 +43,7 @@ h1, h2, h3, h4, h5 {
font-weight: normal; font-weight: normal;
} }
h1 { h1, h2 {
font-size: 16px;
}
h2 {
color: var(--default);
font-size: 14px; font-size: 14px;
} }

View file

@ -4,7 +4,7 @@
{% for row in rows %} {% for row in rows %}
<div> <div>
<a name="{{ row.torrent.info_hash }}"></a> <a name="{{ row.torrent.info_hash }}"></a>
<h2>{{ row.torrent.name }}</h2> <h2><a href="/{{ row.torrent.info_hash }}">{{ row.torrent.name }}</a></h2>
{% if row.torrent.comment %}<p>{{ row.torrent.comment }}</p>{% endif %} {% if row.torrent.comment %}<p>{{ row.torrent.comment }}</p>{% endif %}
<div> <div>
<ul> <ul>

27
templates/info.html.tera Normal file
View file

@ -0,0 +1,27 @@
{% extends "layout/default" %}
{% block content %}
{% if torrent %}
<div>
<h1>{% if torrent.name %}{{ torrent.name }}{% else %}{{ info_hash }}{% endif %}</h1>
{% if torrent.comment %}<p>{{ torrent.comment }}</p>{% endif %}
<div>
<ul>
<li><span title="Indexed">{{ torrent.indexed }}</span></li>
{% if torrent.created %}<li><span title="Created">({{ torrent.created }})</span></li>{% endif %}
{% if torrent.size %}<li><span title="Size">{{ torrent.size }}</span></li>{% endif %}
<li><span title="Files">{{ torrent.files }}</span></li>
{% if torrent.scrape %}
<li><span title="Seeders" class="seeders">{{ torrent.scrape.seeders }}</span></li>
<li><span title="Peers" class="peers">{{ torrent.scrape.peers }}</span></li>
<li><span title="Leechers" class="leechers">{{ torrent.scrape.leechers }}</span></li>
{% endif %}
</ul>
<div>
<a rel="nofollow" href="{{ torrent.magnet }}" title="Get magnet" class="action magnet"></a>
</div>
</div>
</div>
{% else %}
<div>Nothing.</div>
{% endif %}
{% endblock content %}