nyaa 0.9.1

A tui tool for browsing and downloading torrents from nyaa.si
Documentation
use std::{error::Error, fs};

use serde::{Deserialize, Serialize};
use transmission_rpc::{
    types::{BasicAuth, TorrentAddArgs},
    TransClient,
};

use crate::{source::Item, util::conv::add_protocol};

use super::{multidownload, ClientConfig, DownloadClient, DownloadError, DownloadResult};

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i8)]
pub enum Priority {
    Low = -1,
    Normal = 0,
    High = 1,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct TransmissionConfig {
    pub base_url: String,
    pub username: Option<String>,
    pub password: Option<String>,
    pub password_file: Option<String>,
    pub use_magnet: Option<bool>,
    pub labels: Option<Vec<String>>,
    pub paused: Option<bool>,
    pub peer_limit: Option<i64>,
    pub download_dir: Option<String>,
    pub bandwidth_priority: Option<Priority>,
}

pub struct TransmissionClient;

impl Default for TransmissionConfig {
    fn default() -> Self {
        Self {
            base_url: "http://localhost:9091/transmission/rpc".to_owned(),
            username: None,
            password: None,
            password_file: None,
            use_magnet: None,
            labels: None,
            paused: None,
            peer_limit: None,
            download_dir: None,
            bandwidth_priority: None,
        }
    }
}

impl TransmissionConfig {
    fn to_form(&self, link: String) -> TorrentAddArgs {
        TorrentAddArgs {
            filename: Some(link),
            labels: self.labels.to_owned(),
            paused: self.paused,
            peer_limit: self.peer_limit,
            download_dir: self.download_dir.to_owned(),
            bandwidth_priority: self.bandwidth_priority.map(|r| r as i64),
            ..Default::default()
        }
    }
}

async fn add_torrent(
    conf: &TransmissionConfig,
    link: String,
    client: reqwest::Client,
) -> Result<(), Box<dyn Error + Send + Sync>> {
    let base_url = add_protocol(conf.base_url.clone(), false)?;
    let mut client = TransClient::new_with_client(base_url, client);

    let pass = match conf.password.as_ref() {
        Some(pass) => Some(pass.to_owned()),
        None => match conf.password_file.as_ref() {
            Some(file) => {
                let contents = fs::read_to_string(file)?;
                let expand = shellexpand::full(contents.trim())?;
                Some(expand.to_string())
            }
            None => None,
        },
    };
    if let (Some(user), Some(password)) = (conf.username.as_ref(), pass) {
        client.set_auth(BasicAuth {
            user: user.clone(),
            password: password.clone(),
        });
    }
    let add = conf.clone().to_form(link);
    client
        .torrent_add(add)
        .await
        .map_err(|e| format!("Failed to add torrent:\n{}", e))?;
    Ok(())
}

pub fn load_config(cfg: &mut ClientConfig) {
    if cfg.transmission.is_none() {
        cfg.transmission = Some(TransmissionConfig::default());
    }
}

impl DownloadClient for TransmissionClient {
    async fn download(item: Item, conf: ClientConfig, client: reqwest::Client) -> DownloadResult {
        let Some(conf) = conf.transmission.clone() else {
            return DownloadResult::error(DownloadError(
                "Failed to get configuration for transmission".to_owned(),
            ));
        };

        if let Some(labels) = conf.labels.clone() {
            if let Some(bad) = labels.iter().find(|l| l.contains(',')) {
                let bad = format!("\"{}\"", bad);
                return DownloadResult::error(DownloadError(format!(
                    "Transmission labels must not contain commas:\n{}",
                    bad
                )));
            }
        }

        let link = match conf.use_magnet {
            None | Some(true) => item.magnet_link.to_owned(),
            Some(false) => item.torrent_link.to_owned(),
        };
        if let Err(e) = add_torrent(&conf, link, client).await {
            return DownloadResult::error(DownloadError(e.to_string()));
        }
        DownloadResult::new(
            "Successfully sent torrent to Transmission".to_owned(),
            vec![item.id],
            vec![],
            false,
        )
    }

    async fn batch_download(
        items: Vec<Item>,
        conf: ClientConfig,
        client: reqwest::Client,
    ) -> DownloadResult {
        multidownload::<TransmissionClient, _>(
            |s| format!("Successfully sent {} torrents to Transmission", s),
            &items,
            &conf,
            &client,
        )
        .await
    }
}