refactor: replace magic indices with RtorrentField enum for type-safe parsing
All checks were successful
Build MIPS Binary / build (push) Successful in 4m22s

This commit is contained in:
spinline
2026-02-08 22:50:26 +03:00
parent 129a4c7586
commit ce10c5dfb2
3 changed files with 99 additions and 81 deletions

View File

@@ -42,3 +42,4 @@ anyhow = "1.0.101"
time = { version = "0.3.47", features = ["serde", "formatting", "parsing"] }
tower_governor = "0.8.0"
governor = "0.10.4"
strum = { version = "0.25", features = ["derive", "strum_macros"] }

View File

@@ -7,95 +7,80 @@ use axum::response::sse::{Event, Sse};
use futures::stream::{self, Stream};
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
use std::convert::Infallible;
use strum::{Display, EnumString};
use tokio_stream::StreamExt;
// Field definitions to keep query and parser in sync
mod fields {
pub const IDX_HASH: usize = 0;
pub const CMD_HASH: &str = "d.hash=";
pub const IDX_NAME: usize = 1;
pub const CMD_NAME: &str = "d.name=";
pub const IDX_SIZE: usize = 2;
pub const CMD_SIZE: &str = "d.size_bytes=";
pub const IDX_COMPLETED: usize = 3;
pub const CMD_COMPLETED: &str = "d.bytes_done=";
pub const IDX_DOWN_RATE: usize = 4;
pub const CMD_DOWN_RATE: &str = "d.down.rate=";
pub const IDX_UP_RATE: usize = 5;
pub const CMD_UP_RATE: &str = "d.up.rate=";
pub const IDX_STATE: usize = 6;
pub const CMD_STATE: &str = "d.state=";
pub const IDX_COMPLETE: usize = 7;
pub const CMD_COMPLETE: &str = "d.complete=";
pub const IDX_MESSAGE: usize = 8;
pub const CMD_MESSAGE: &str = "d.message=";
pub const IDX_LEFT_BYTES: usize = 9;
pub const CMD_LEFT_BYTES: &str = "d.left_bytes=";
pub const IDX_CREATION_DATE: usize = 10;
pub const CMD_CREATION_DATE: &str = "d.creation_date=";
pub const IDX_HASHING: usize = 11;
pub const CMD_HASHING: &str = "d.hashing=";
pub const IDX_LABEL: usize = 12;
pub const CMD_LABEL: &str = "d.custom1=";
#[derive(Debug, Clone, Copy, EnumString, Display, PartialEq)]
enum RtorrentField {
#[strum(serialize = "d.hash=")]
Hash,
#[strum(serialize = "d.name=")]
Name,
#[strum(serialize = "d.size_bytes=")]
Size,
#[strum(serialize = "d.bytes_done=")]
Completed,
#[strum(serialize = "d.down.rate=")]
DownRate,
#[strum(serialize = "d.up.rate=")]
UpRate,
#[strum(serialize = "d.state=")]
State,
#[strum(serialize = "d.complete=")]
Complete,
#[strum(serialize = "d.message=")]
Message,
#[strum(serialize = "d.left_bytes=")]
LeftBytes,
#[strum(serialize = "d.creation_date=")]
CreationDate,
#[strum(serialize = "d.hashing=")]
Hashing,
#[strum(serialize = "d.custom1=")]
Label,
}
use fields::*;
// Constants for rTorrent fields to ensure query and parser stay in sync
const RTORRENT_FIELDS: &[&str] = &[
"", // Ignored by multicall pattern
"main", // View
CMD_HASH,
CMD_NAME,
CMD_SIZE,
CMD_COMPLETED,
CMD_DOWN_RATE,
CMD_UP_RATE,
CMD_STATE,
CMD_COMPLETE,
CMD_MESSAGE,
CMD_LEFT_BYTES,
CMD_CREATION_DATE,
CMD_HASHING,
CMD_LABEL,
const RTORRENT_FIELDS: &[RtorrentField] = &[
RtorrentField::Hash,
RtorrentField::Name,
RtorrentField::Size,
RtorrentField::Completed,
RtorrentField::DownRate,
RtorrentField::UpRate,
RtorrentField::State,
RtorrentField::Complete,
RtorrentField::Message,
RtorrentField::LeftBytes,
RtorrentField::CreationDate,
RtorrentField::Hashing,
RtorrentField::Label,
];
fn parse_long(s: Option<&String>) -> i64 {
s.map(|v| v.parse().unwrap_or(0)).unwrap_or(0)
fn get_field_value(row: &Vec<String>, field: RtorrentField) -> String {
let idx = RTORRENT_FIELDS.iter().position(|&f| f == field).unwrap_or(0);
row.get(idx).cloned().unwrap_or_default()
}
fn parse_string(s: Option<&String>) -> String {
s.cloned().unwrap_or_default()
fn parse_long(s: &str) -> i64 {
s.parse().unwrap_or(0)
}
/// Converts a raw row of strings from rTorrent XML-RPC into a generic Torrent struct
fn from_rtorrent_row(row: Vec<String>) -> Torrent {
let hash = parse_string(row.get(IDX_HASH));
let name = parse_string(row.get(IDX_NAME));
let size = parse_long(row.get(IDX_SIZE));
let completed = parse_long(row.get(IDX_COMPLETED));
let down_rate = parse_long(row.get(IDX_DOWN_RATE));
let up_rate = parse_long(row.get(IDX_UP_RATE));
fn from_rtorrent_row(row: &Vec<String>) -> Torrent {
let hash = get_field_value(row, RtorrentField::Hash);
let name = get_field_value(row, RtorrentField::Name);
let size = parse_long(&get_field_value(row, RtorrentField::Size));
let completed = parse_long(&get_field_value(row, RtorrentField::Completed));
let down_rate = parse_long(&get_field_value(row, RtorrentField::DownRate));
let up_rate = parse_long(&get_field_value(row, RtorrentField::UpRate));
let state = parse_long(row.get(IDX_STATE));
let is_complete = parse_long(row.get(IDX_COMPLETE));
let message = parse_string(row.get(IDX_MESSAGE));
let left_bytes = parse_long(row.get(IDX_LEFT_BYTES));
let added_date = parse_long(row.get(IDX_CREATION_DATE));
let is_hashing = parse_long(row.get(IDX_HASHING));
let label_raw = parse_string(row.get(IDX_LABEL));
let state = parse_long(&get_field_value(row, RtorrentField::State));
let is_complete = parse_long(&get_field_value(row, RtorrentField::Complete));
let message = get_field_value(row, RtorrentField::Message);
let left_bytes = parse_long(&get_field_value(row, RtorrentField::LeftBytes));
let added_date = parse_long(&get_field_value(row, RtorrentField::CreationDate));
let is_hashing = parse_long(&get_field_value(row, RtorrentField::Hashing));
let label_raw = get_field_value(row, RtorrentField::Label);
let label = if label_raw.is_empty() {
None
@@ -146,7 +131,10 @@ fn from_rtorrent_row(row: Vec<String>) -> Torrent {
}
pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, XmlRpcError> {
let params: Vec<RpcParam> = RTORRENT_FIELDS.iter().map(|s| RpcParam::from(*s)).collect();
let params: Vec<RpcParam> = RTORRENT_FIELDS
.iter()
.map(|&f| RpcParam::from(f.to_string()))
.collect();
let xml = client.call("d.multicall2", &params).await?;
if xml.trim().is_empty() {
@@ -155,7 +143,7 @@ pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, Xml
let rows = parse_multicall_response(&xml)?;
let torrents = rows.into_iter().map(from_rtorrent_row).collect();
let torrents = rows.iter().map(from_rtorrent_row).collect();
Ok(torrents)
}