implement magnet links

This commit is contained in:
yggverse 2025-08-05 15:06:44 +03:00
parent ec6d9a4e00
commit 7da285ca69
5 changed files with 58 additions and 31 deletions

View file

@ -7,8 +7,7 @@ pub struct Feed {
description: Option<String>, description: Option<String>,
link: Option<String>, link: Option<String>,
title: String, title: String,
/// Valid, parsed from Url, ready-to-use address string donor trackers: Option<HashSet<Url>>,
trackers: Option<HashSet<String>>,
} }
impl Feed { impl Feed {
@ -22,7 +21,7 @@ impl Feed {
description: description.map(escape), description: description.map(escape),
link: link.map(|s| escape(s.to_string())), link: link.map(|s| escape(s.to_string())),
title: escape(title), title: escape(title),
trackers: trackers.map(|v| v.into_iter().map(|u| u.to_string()).collect()), trackers,
} }
} }
@ -70,7 +69,7 @@ impl Feed {
.map(|b| b.to_string()) .map(|b| b.to_string())
.unwrap_or("?".into()) // @TODO .unwrap_or("?".into()) // @TODO
), ),
escape(self.magnet(&torrent.info_hash)) escape(format::magnet(&torrent.info_hash, self.trackers.as_ref()))
)); ));
if let Some(d) = item_description(torrent.length, torrent.files) { if let Some(d) = item_description(torrent.length, torrent.files) {
@ -91,23 +90,6 @@ impl Feed {
buffer.push_str("</channel></rss>"); buffer.push_str("</channel></rss>");
buffer buffer
} }
// Tools
fn magnet(&self, info_hash: &str) -> String {
let mut b = if info_hash.len() == 40 {
format!("magnet:?xt=urn:btih:{info_hash}")
} else {
todo!("info-hash v2 is not supported by librqbit")
};
if let Some(ref trackers) = self.trackers {
for tracker in trackers {
b.push_str("&tr=");
b.push_str(&urlencoding::encode(tracker))
}
}
b
}
} }
fn escape(subject: String) -> String { fn escape(subject: String) -> String {

View file

@ -15,3 +15,18 @@ pub fn bytes(value: u64) -> String {
format!("{:.2} GB", f / GB) format!("{:.2} GB", f / GB)
} }
} }
pub fn magnet(info_hash: &str, trackers: Option<&std::collections::HashSet<url::Url>>) -> String {
let mut b = if info_hash.len() == 40 {
format!("magnet:?xt=urn:btih:{info_hash}")
} else {
todo!("info-hash v2 is not supported by librqbit")
};
if let Some(t) = trackers {
for tracker in t {
b.push_str("&tr=");
b.push_str(&urlencoding::encode(tracker.as_str()))
}
}
b
}

View file

@ -15,7 +15,8 @@ use rocket::{
serde::Serialize, serde::Serialize,
}; };
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{Template, context};
use storage::{Order, Sort, Storage}; use std::collections::HashSet;
use storage::{Order, Sort, Storage, Torrent};
use url::Url; use url::Url;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -25,21 +26,33 @@ pub struct Meta {
pub description: Option<String>, pub description: Option<String>,
pub stats: Option<Url>, pub stats: Option<Url>,
pub title: String, pub title: String,
pub trackers: Option<Vec<Url>>, pub trackers: Option<HashSet<Url>>,
} }
#[get("/")] #[get("/")]
fn index(storage: &State<Storage>, meta: &State<Meta>) -> Result<Template, Custom<String>> { fn index(storage: &State<Storage>, meta: &State<Meta>) -> Result<Template, Custom<String>> {
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Row {
torrent: Torrent,
magnet: String,
}
Ok(Template::render( Ok(Template::render(
"index", "index",
context! { context! {
meta: meta.inner(), meta: meta.inner(),
torrents: storage rows: storage
.torrents( .torrents(
Some((Sort::Modified, Order::Asc)), Some((Sort::Modified, Order::Asc)),
Some(storage.default_limit), Some(storage.default_limit),
) )
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))? .map_err(|e| Custom(Status::InternalServerError, e.to_string()))?
.into_iter()
.map(|torrent| Row {
magnet: format::magnet(&torrent.info_hash, meta.trackers.as_ref()),
torrent,
})
.collect::<Vec<Row>>()
}, },
)) ))
} }
@ -67,7 +80,7 @@ fn rocket() -> _ {
config.title.clone(), config.title.clone(),
config.description.clone(), config.description.clone(),
config.link.clone(), config.link.clone(),
config.tracker.clone().map(|u| u.into_iter().collect()), // make sure it's unique config.tracker.clone().map(|u| u.into_iter().collect()),
); );
let storage = Storage::init(config.storage, config.limit, config.capacity).unwrap(); // @TODO handle let storage = Storage::init(config.storage, config.limit, config.capacity).unwrap(); // @TODO handle
rocket::build() rocket::build()
@ -84,7 +97,7 @@ fn rocket() -> _ {
description: config.description, description: config.description,
stats: config.stats, stats: config.stats,
title: config.title, title: config.title,
trackers: config.tracker, trackers: config.tracker.map(|u| u.into_iter().collect()),
}) })
.mount("/", routes![index, rss]) .mount("/", routes![index, rss])
} }

View file

@ -1,9 +1,16 @@
{% extends "layout/default" %} {% extends "layout/default" %}
{% block content %} {% block content %}
{% for torrent in torrents %} {% for row in rows %}
<div> <div>
<a name="{{ torrent.info_hash }}"></a> <a name="{{ row.torrent.info_hash }}"></a>
<h2>{{ torrent.name }}</h2> <h2>{{ row.torrent.name }}</h2>
</div> <div>
<a rel="nofollow" href="{{ row.magnet }}" title="Magnet">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M15 12h-4v3h4v-3ZM5 12H1v3h4v-3ZM0 8a8 8 0 1 1 16 0v8h-6V8a2 2 0 1 0-4 0v8H0V8Z"/>
</svg>
</a>
</div>
</div>
{% endfor %} {% endfor %}
{% endblock content %} {% endblock content %}

View file

@ -86,12 +86,22 @@
margin: 0 auto; margin: 0 auto;
} }
/* item row */
main > div { main > div {
background-color: #34384f; background-color: #34384f;
border-radius: 3px; border-radius: 3px;
margin: 8px 0; margin: 8px 0;
padding: 24px; padding: 24px;
} }
/* controls */
main > div > div {
float: right;
}
main > div > div > a> svg {
vertical-align: middle;
}
</style> </style>
</head> </head>
<body> <body>