torman/src/main.rs
2024-06-18 21:03:08 +03:00

247 lines
No EOL
7.3 KiB
Rust

use std::{env, fmt::Debug, fs::{self, DirEntry}, path::Path};
use bencode::{decode, Value};
use clap::{Parser, Subcommand};
use rusqlite::Connection;
use dotenv::dotenv;
mod bencode;
#[derive(Subcommand)]
enum Command {
Index {
#[arg(short='p', long, default_value="torman.db", help="path to transmission dir with \"torrents\" and \"resume\" dirs")]
path: String
},
Scrape {
#[arg(short='f', long, help="filter torrents by their destination field")]
destination: String
}
}
#[derive(Parser)]
#[command(version, about)]
struct Args {
#[arg(short='d', long, help="path to sqlite database")]
db_path: String,
#[command(subcommand)]
command: Command
}
#[derive(Debug)]
enum TorrentLogicError {
NoTorrentName,
NoInfoDict,
NoPathList
}
fn get_torrent_files(torrent: &Value) -> Result<Vec<String>, TorrentLogicError> {
let info = torrent.get_value("info")
.ok_or(TorrentLogicError::NoInfoDict)?;
let mut files: Vec<String> = Vec::new();
if let Some(file_list_v1) = info.get_list("files") {
// multiple files v1
let root = info.get_string("name")
.ok_or(TorrentLogicError::NoTorrentName)?;
for file_object in file_list_v1 {
let mut path_list = file_object.get_string_list("path")
.ok_or(TorrentLogicError::NoPathList)?;
path_list.insert(0, root.clone());
files.push(path_list.join("/"));
}
} else if let Some(file_dict_v2) = info.get_dict("file tree") {
// single file / multiple files v2
let root = info.get_string("name")
.ok_or(TorrentLogicError::NoTorrentName)?;
let files_v2: Vec<String> = file_dict_v2
.keys()
.into_iter()
.filter_map(|k| k.to_string())
.collect();
if files_v2.len() > 1 {
// multiple
for file in files_v2 {
let mut path = root.clone();
path.push_str("/");
path.push_str(&file);
files.push(path);
}
} else {
// single
files.push(files_v2.first().unwrap().clone());
}
} else {
// single file v1
let single = info.get_string("name")
.ok_or(TorrentLogicError::NoTorrentName)?;
files.push(single);
}
Ok(files)
}
fn index(db: Connection, path: &String) {
// iterate resume files
let entries: Vec<DirEntry> = fs::read_dir(Path::new(path).join("resume"))
.unwrap()
.filter_map(|f| f.ok())
.collect();
for entry in entries {
let file_type = match entry.file_type() {
Ok(file_type) => file_type,
Err(_) => continue
};
if !file_type.is_file() {
continue;
}
let hash = match Path::new(&entry.file_name()).file_stem() {
Some(stem) => match stem.to_os_string().into_string() {
Ok(str) => str,
Err(os_str) => {
eprintln!("failed to convert file name for {:#?}", os_str);
continue;
}
},
None => {
eprintln!("file {:#?} has no extension or conversion failed", entry.file_name());
continue;
}
};
// parse the resume file
let (resume, _) = {
let resume_data = match fs::read(entry.path()) {
Ok(data) => data,
Err(_) => {
eprintln!("failed to read {} resume file", hash);
continue
}
};
match decode(&resume_data) {
Ok(value) => value,
Err(e) => {
eprintln!("failed to parse {} resume file: {:#?}", hash, e);
continue
}
}
};
// parse the torrent file
let torrent_path = {
let mut torrent_name = hash.to_owned();
torrent_name.push_str(".torrent");
Path::new(path).join("torrents").join(torrent_name)
};
let (torrent, _) = {
let torrent_data = match fs::read(torrent_path) {
Ok(data) => data,
Err(_) => {
eprintln!("failed to read {} torrent file", hash);
continue
}
};
match decode(&torrent_data) {
Ok(value) => value,
Err(e) => {
eprintln!("failed to parse {} torrent file: {:#?}", hash, e);
continue
}
}
};
// make table row
let hash = hash;
let name = resume.get_string("name");
let destination = resume.get_string("destination");
let downloaded = resume.get_integer("downloaded");
let uploaded = resume.get_integer("uploaded");
let announce = torrent.get_string("announce");
let comment = torrent.get_string("comment");
let created_by = torrent.get_string("created_by");
let creation_date = torrent.get_integer("creation_date");
let publisher = torrent.get_string("publisher");
let publisher_url = torrent.get_string("publisher-url");
// get torrent files
let files = match get_torrent_files(&torrent) {
Ok(files) => files,
Err(e) => {
eprintln!("can't get file list for {}: {:#?}", hash, e);
continue;
}
};
// create torrent record
let id = db.query_row("INSERT INTO torrent (
hash, name, destination,
downloaded, uploaded,
announce, comment,
created_by, creation_date,
publisher, publisher_url)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
RETURNING id;", (
hash, name, destination,
downloaded, uploaded,
announce, comment,
created_by, creation_date,
publisher, publisher_url
),
|row| Ok(row.get::<usize, i64>(0)?)
).expect("insert failed");
// we're using here unwrap/expect since we want full program crash
// to debug any sql bugs
// insert torrent files
for file in files {
db.execute("INSERT INTO file (torrent_id,file_name) VALUES (?,?);", (id, file))
.expect("failed to insert file!");
}
}
}
struct FilteredTorrent {
pub id: i64,
pub publisher_url: String
}
fn scrape(db: Connection, destination: &String) {
let mut stmt = db.prepare("SELECT id,publisher_url FROM torrent WHERE destination = ?1;").unwrap();
let torrents = stmt.query_map([destination], |row| {
Ok(FilteredTorrent {
id: row.get(0)?,
publisher_url: row.get(1)?
})
})
.expect("query_map")
.filter_map(|f| f.ok());
for torrent in torrents {
dbg!(torrent.id, torrent.publisher_url);
}
}
fn main() {
dotenv().ok();
let args = Args::parse();
let db = Connection::open(args.db_path).unwrap();
match &args.command {
Command::Index { path } => {
index(db, path);
},
Command::Scrape { destination } => {
scrape(db, destination);
}
}
}