transmission-gobject-0.1.6/.cargo_vcs_info.json0000644000000001360000000000100152040ustar { "git": { "sha1": "61c23c97ea200c12db455c036bdadd67a99be0db" }, "path_in_vcs": "" }transmission-gobject-0.1.6/.gitignore000064400000000000000000000000231046102023000157570ustar 00000000000000/target Cargo.lock transmission-gobject-0.1.6/Cargo.toml0000644000000023470000000000100132100ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "transmission-gobject" version = "0.1.6" authors = ["Felix Häcker "] description = "gtk-rs gobject wrapper for transmission-client crate" homepage = "https://gitlab.gnome.org/haecker-felix/transmission-gobject" documentation = "https://docs.rs/transmission-gobject" license = "MIT" repository = "https://gitlab.gnome.org/haecker-felix/transmission-gobject" [dependencies.async-channel] version = "2.3" [dependencies.gio] version = "0.20" [dependencies.glib] version = "0.20" [dependencies.indexmap] version = "2.2" [dependencies.log] version = "0.4" [dependencies.num_enum] version = "0.7" [dependencies.once_cell] version = "1.19" [dependencies.transmission-client] version = "0.1.5" [dependencies.url] version = "2.5" transmission-gobject-0.1.6/Cargo.toml.orig000064400000000000000000000011141046102023000166600ustar 00000000000000[package] name = "transmission-gobject" version = "0.1.6" authors = ["Felix Häcker "] edition = "2021" description = "gtk-rs gobject wrapper for transmission-client crate" license = "MIT" documentation = "https://docs.rs/transmission-gobject" homepage = "https://gitlab.gnome.org/haecker-felix/transmission-gobject" repository = "https://gitlab.gnome.org/haecker-felix/transmission-gobject" [dependencies] once_cell = "1.19" glib = "0.20" gio = "0.20" url = "2.5" log = "0.4" num_enum = "0.7" transmission-client = "0.1.5" async-channel = "2.3" indexmap = "2.2" transmission-gobject-0.1.6/src/authentication.rs000064400000000000000000000021061046102023000201470ustar 00000000000000use std::cell::OnceCell; use glib::object::ObjectExt; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::Properties; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrAuthentication)] pub struct TrAuthentication { #[property(get, set, construct_only)] pub username: OnceCell, #[property(get, set, construct_only)] pub password: OnceCell, } #[glib::object_subclass] impl ObjectSubclass for TrAuthentication { const NAME: &'static str = "TrAuthentication"; type Type = super::TrAuthentication; type ParentType = glib::Object; } #[glib::derived_properties] impl ObjectImpl for TrAuthentication {} } glib::wrapper! { pub struct TrAuthentication(ObjectSubclass); } impl TrAuthentication { pub fn new(username: &str, password: &str) -> Self { glib::Object::builder() .property("username", username) .property("password", password) .build() } } transmission-gobject-0.1.6/src/client.rs000064400000000000000000000514621046102023000164170ustar 00000000000000use std::cell::{Cell, OnceCell, RefCell}; use std::collections::BTreeMap; use std::time::Duration; use async_channel::{Receiver, Sender}; use gio::prelude::*; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::subclass::Signal; use glib::{clone, Properties, SourceId}; use once_cell::sync::Lazy; use transmission_client::{ Authentication, Client, ClientError, Torrent, TorrentFiles, TorrentPeers, }; use url::Url; use crate::{TrAuthentication, TrSession, TrSessionStats, TrTorrent, TrTorrentModel}; mod imp { use super::*; #[derive(Properties)] #[properties(wrapper_type = super::TrClient)] pub struct TrClient { #[property(get)] address: RefCell, #[property(get=Self::polling_rate, set=Self::set_polling_rate)] polling_rate: Cell, #[property(get)] torrents: TrTorrentModel, #[property(get)] session: OnceCell, #[property(get)] session_stats: TrSessionStats, #[property(get = Self::is_busy)] is_busy: std::marker::PhantomData, #[property(get = Self::is_connected)] is_connected: std::marker::PhantomData, pub client: RefCell>, pub polling_source_id: RefCell>, pub authentication: RefCell>, pub do_connect: Cell, pub do_disconnect: Cell, pub sender: Sender, pub receiver: Receiver, } impl TrClient { fn is_busy(&self) -> bool { self.do_connect.get() || self.do_disconnect.get() } fn is_connected(&self) -> bool { self.client.borrow().is_some() } } #[glib::object_subclass] impl ObjectSubclass for TrClient { const NAME: &'static str = "TrClient"; type ParentType = glib::Object; type Type = super::TrClient; fn new() -> Self { let (sender, receiver) = async_channel::bounded(1); Self { address: RefCell::default(), polling_rate: Cell::new(1000), torrents: TrTorrentModel::default(), session: OnceCell::default(), session_stats: TrSessionStats::default(), is_busy: Default::default(), is_connected: Default::default(), client: RefCell::default(), polling_source_id: RefCell::default(), authentication: RefCell::default(), do_connect: Cell::default(), do_disconnect: Cell::default(), sender, receiver, } } } #[glib::derived_properties] impl ObjectImpl for TrClient { fn constructed(&self) { self.parent_constructed(); let session = TrSession::new(&self.obj()); self.session.set(session).unwrap(); } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![ Signal::builder("connection-failure").build(), Signal::builder("torrent-added") .param_types([TrTorrent::static_type()]) .build(), Signal::builder("torrent-downloaded") .param_types([TrTorrent::static_type()]) .build(), ] }); SIGNALS.as_ref() } } impl TrClient { pub async fn connect_internal(&self, address: String) -> Result<(), ClientError> { if self.is_connected() { self.obj().disconnect(false).await; } debug!("Connect to {} ...", address); // Check if the address has changed, and delete cached auth information if // needed if address != self.obj().address() { *self.authentication.borrow_mut() = None; } // Create new client for the rpc communication let url = Url::parse(&address).unwrap(); let client = Client::new(url); // Authenticate if we have information... if self.authentication.borrow().is_some() { self.rpc_auth(Some(client.clone())); } // Set properties self.address.borrow_mut().clone_from(&address); self.obj().notify_address(); // Check server version let session = client.session().await?; debug!( "Connected to transmission session version {}", session.version ); // Make first poll *self.client.borrow_mut() = Some(client.clone()); self.poll_data(true).await?; self.obj().notify_is_connected(); // Start polling self.start_polling(); Ok(()) } pub fn start_polling(&self) { if let Some(id) = self.polling_source_id.borrow_mut().take() { warn!("Polling source id wasn't None"); id.remove(); } let duration = Duration::from_millis(self.polling_rate.get()); let id = glib::source::timeout_add_local( duration, clone!( #[weak(rename_to = this)] self, #[upgrade_or_panic] move || { let disconnect = this.do_disconnect.get(); if disconnect { debug!("Stop polling loop..."); // Send message back to the disconnect method to indicate that the // polling loop has been stopped let sender = this.sender.clone(); sender.send_blocking(true).unwrap(); this.do_disconnect.set(false); this.obj().notify_is_busy(); } else { let fut = clone!( #[weak] this, async move { debug!("Poll... ({}ms)", this.polling_rate.get()); this.obj().refresh_data().await; } ); glib::spawn_future_local(fut); } if disconnect { *this.polling_source_id.borrow_mut() = None; glib::ControlFlow::Break } else { glib::ControlFlow::Continue } } ), ); *self.polling_source_id.borrow_mut() = Some(id); } pub fn rpc_auth(&self, rpc_client: Option) { if let Some(auth) = &*self.authentication.borrow() { if let Some(rpc_client) = rpc_client { let rpc_auth = Authentication { username: auth.username(), password: auth.password(), }; rpc_client.set_authentication(Some(rpc_auth)); } else { warn!("Unable to authenticate, no rpc connection"); } } else { warn!("Unable to authenticate, no information stored"); } } pub async fn poll_data(&self, is_initial_poll: bool) -> Result<(), ClientError> { if let Some(client) = self.obj().rpc_client() { let torrents = client.torrents(None).await?; // Transmission assigns each torrent a "global" queue position, which means // *every* torrent has a queue position, ignoring the torrent status. Therefore // that position doesn't match the actual position of the download / seed queue, // so we determine the position and set it as as property of TrTorrent. let download_queue = Self::sorted_queue_by_status(&torrents, 3); // 3 -> DownloadWait let seed_queue = Self::sorted_queue_by_status(&torrents, 5); // 5 -> SeedWait // We have to find out which torrent isn't included anymore, // so we can remove it from the model too. let mut hashes_delta = self.torrents.get_hashes(); // Count downloaded torrents so we can use that value for TrSessionStats let mut downloaded_torrents = 0; for rpc_torrent in torrents { let hash = rpc_torrent.hash_string.clone(); let download_queue_pos = Self::queue_pos(&download_queue, &rpc_torrent); let seed_queue_pos = Self::queue_pos(&seed_queue, &rpc_torrent); // Increase downloaded torrents count if it's SeedWait(5) or Seed(6) if rpc_torrent.status == 5 || rpc_torrent.status == 6 { downloaded_torrents += 1; } if let Some(torrent) = self.torrents.torrent_by_hash(hash.clone()) { let id = Some(vec![rpc_torrent.id]); // Update the download / seed queue position torrent.refresh_queue_positions(download_queue_pos, seed_queue_pos); // Only retrieve and deserialize files/peers information // on demand to save resources. Often those information aren't needed. let (torrent_files, torrent_peers) = if torrent.update_extra_info() { ( client .torrents_files(id.clone()) .await? .first() .cloned() .unwrap_or_default(), client .torrents_peers(id) .await? .first() .cloned() .unwrap_or_default(), ) } else { (TorrentFiles::default(), TorrentPeers::default()) }; // Check if torrent status or "global" queue position changed if torrent.status() as u32 != rpc_torrent.status as u32 || torrent.queue_position() != rpc_torrent.queue_position { // Update torrent properties torrent.refresh_values(&rpc_torrent, &torrent_files, &torrent_peers); // ... yes -> emit torrent model `status-changed` signal self.torrents.emit_by_name::<()>("status-changed", &[]); } else { // Update torrent properties, without emiting the status-changed signal torrent.refresh_values(&rpc_torrent, &torrent_files, &torrent_peers); } let index = hashes_delta.iter().position(|x| *x == hash).unwrap(); hashes_delta.remove(index); } else { debug!("Add new torrent: {}", rpc_torrent.name); let torrent = TrTorrent::from_rpc_torrent(&rpc_torrent, &self.obj()); torrent.refresh_queue_positions(download_queue_pos, seed_queue_pos); // Set dynamic values torrent.refresh_values( &rpc_torrent, &TorrentFiles::default(), &TorrentPeers::default(), ); self.torrents.add_torrent(&torrent); if !is_initial_poll { self.obj().emit_by_name::<()>("torrent-added", &[&torrent]); } } } // Remove the deltas from the model for hash in hashes_delta { let torrent = self.torrents.torrent_by_hash(hash).unwrap(); self.torrents.remove_torrent(&torrent); } let rpc_session = client.session().await?; self.obj().session().refresh_values(rpc_session); let rpc_session_stats = client.session_stats().await?; self.obj() .session_stats() .refresh_values(rpc_session_stats, downloaded_torrents); } else { warn!("Unable to poll transmission data, no rpc connection."); } Ok(()) } pub fn sorted_queue_by_status(rpc_torrents: &[Torrent], status_code: i32) -> Vec { let mut download_queue: BTreeMap = BTreeMap::new(); // Get all torrents with given status code for torrent in rpc_torrents { if torrent.status == status_code { download_queue.insert(torrent.queue_position.try_into().unwrap(), torrent); } } // Insert the sorted torrents into a vec let mut result = Vec::new(); for torrent in download_queue { result.push(torrent.1.clone()); } result } pub fn queue_pos(queue: &[Torrent], rpc_torrent: &Torrent) -> i32 { queue .iter() .position(|t| t == rpc_torrent) .map(|pos| pos as i32) .unwrap_or(-1) } pub fn polling_rate(&self) -> u64 { self.polling_rate.get() } pub fn set_polling_rate(&self, ms: u64) { self.polling_rate.set(ms); let id = self.polling_source_id.borrow_mut().take(); if let Some(id) = id { id.remove(); self.start_polling(); } } } } glib::wrapper! { pub struct TrClient(ObjectSubclass); } impl Default for TrClient { fn default() -> Self { Self::new() } } impl TrClient { pub fn new() -> Self { glib::Object::new() } pub async fn test_connectivity( address: String, auth: Option, ) -> Result<(), ClientError> { let url = Url::parse(&address).unwrap(); let rpc_client = Client::new(url); if let Some(auth) = auth { let rpc_auth = Authentication { username: auth.username(), password: auth.password(), }; rpc_client.set_authentication(Some(rpc_auth)); } let session = rpc_client.session().await?; debug!( "Connectivity test to {} succeeded: Transmission daemon version {}", address, session.version ); Ok(()) } pub async fn test_port(&self) -> Result { if let Some(rpc_client) = self.rpc_client() { rpc_client.port_test().await } else { warn!("Unable to test port, no rpc connection"); Ok(false) } } pub async fn connect(&self, address: String) -> Result<(), ClientError> { let imp = self.imp(); if self.is_busy() { warn!("Client is currently busy, unable to connect to new address."); return Ok(()); } // With the do_connect/disconnect variables we avoid race conditions // eg. doing another connect attempt, while the client is already busy imp.do_connect.set(true); self.notify_is_busy(); // Do the actual connecting work let result = imp.connect_internal(address).await; // Work is done -> unblock it again. imp.do_connect.set(false); self.notify_is_busy(); result } pub async fn disconnect(&self, close_session: bool) { let imp = self.imp(); if !self.is_connected() { warn!("Unable to disconnect, is not connected."); return; } if close_session { let client = imp.client.borrow().as_ref().unwrap().clone(); client.session_close().await.unwrap(); } let _client = imp.client.borrow_mut().take().unwrap(); self.notify_is_connected(); // Wait till polling loop has stopped imp.do_disconnect.set(true); self.notify_is_busy(); // Wait from message from polling loop to ensure we're not polling anymore. let r = imp.receiver.recv().await.unwrap(); debug!("Stopped polling: {:?}", r); // Not connected anymore -> clear torrents model self.torrents().clear(); debug!("Disconnected from transmission session"); } pub fn set_authentication(&self, auth: TrAuthentication) { let imp = self.imp(); *imp.authentication.borrow_mut() = Some(auth); imp.rpc_auth(self.rpc_client()); } /// `filename` can be a magnet url or a local file path pub async fn add_torrent_by_filename(&self, filename: String) -> Result<(), ClientError> { if let Some(client) = self.rpc_client() { client.torrent_add_filename(&filename).await?; self.refresh_data().await; } else { warn!("Unable to add new torrent, no rpc connection"); } Ok(()) } /// `metainfo` is the base64 encoded content of a .torrent file pub async fn add_torrent_by_metainfo(&self, metainfo: String) -> Result<(), ClientError> { if let Some(client) = self.rpc_client() { client.torrent_add_metainfo(&metainfo).await?; self.refresh_data().await; } else { warn!("Unable to add new torrent, no rpc connection"); } Ok(()) } pub async fn remove_torrents( &self, only_downloaded: bool, delete_local_data: bool, ) -> Result<(), ClientError> { let model = self.torrents(); let mut remove_hashes = vec![]; for i in 0..model.n_items() { let torrent = model.item(i).unwrap().downcast::().unwrap(); if !only_downloaded || (torrent.size() == torrent.downloaded() && torrent.downloaded() != 0) { remove_hashes.push(torrent.hash()); } } if let Some(client) = self.rpc_client() { client .torrent_remove(Some(remove_hashes), delete_local_data) .await?; self.refresh_data().await; } else { warn!("Unable to remove torrents, no rpc connection"); } Ok(()) } pub async fn start_torrents(&self) -> Result<(), ClientError> { let model = self.torrents(); let mut start_hashes = vec![]; // We need to get the hashes of the torrents to be able to start all of them for i in 0..model.n_items() { let torrent = model.item(i).unwrap().downcast::().unwrap(); start_hashes.push(torrent.hash()); } if let Some(client) = self.rpc_client() { client.torrent_start(Some(start_hashes), false).await?; self.refresh_data().await; } else { warn!("Unable to start torrents, no rpc connection") } Ok(()) } pub async fn stop_torrents(&self) -> Result<(), ClientError> { let model = self.torrents(); let mut stop_hashes = vec![]; for i in 0..model.n_items() { let torrent = model.item(i).unwrap().downcast::().unwrap(); stop_hashes.push(torrent.hash()); } if let Some(client) = self.rpc_client() { client.torrent_stop(Some(stop_hashes)).await?; // The daemon needs a short time to stop the torrents glib::timeout_future(Duration::from_millis(500)).await; self.refresh_data().await; } else { warn!("Unable to stop torrents, no rpc connection"); } Ok(()) } /// Polls the latest information without waiting for the next polling /// timeout pub async fn refresh_data(&self) { if let Err(err) = self.imp().poll_data(false).await { warn!("Couldn't poll transmission data: {}", err.to_string()); self.clone().disconnect(false).await; self.emit_by_name::<()>("connection-failure", &[]); } } pub(crate) fn rpc_client(&self) -> Option { let imp = self.imp(); imp.client.borrow().clone() } } transmission-gobject-0.1.6/src/encryption.rs000064400000000000000000000021011046102023000173150ustar 00000000000000use glib::Enum; use transmission_client::Encryption; #[derive(Default, Copy, Debug, Clone, PartialEq, Enum)] #[repr(u32)] #[enum_type(name = "TrEncrypption")] pub enum TrEncryption { Required, #[default] Preferred, Tolerated, } impl From for TrEncryption { fn from(u: u32) -> Self { match u { 0 => Self::Required, 1 => Self::Preferred, 2 => Self::Tolerated, _ => Self::default(), } } } impl From for TrEncryption { fn from(enc: Encryption) -> Self { match enc { Encryption::Required => TrEncryption::Required, Encryption::Preferred => TrEncryption::Preferred, Encryption::Tolerated => TrEncryption::Tolerated, } } } impl From for Encryption { fn from(val: TrEncryption) -> Self { match val { TrEncryption::Required => Encryption::Required, TrEncryption::Preferred => Encryption::Preferred, TrEncryption::Tolerated => Encryption::Tolerated, } } } transmission-gobject-0.1.6/src/file.rs000064400000000000000000000243031046102023000160520ustar 00000000000000use std::cell::{Cell, OnceCell, RefCell}; use std::collections::{HashMap, HashSet}; use gio::prelude::*; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::{clone, Properties}; use transmission_client::{File, FileStat}; use crate::{TrRelatedModel, TrTorrent}; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrFile)] pub struct TrFile { #[property(get, set, construct_only)] torrent: OnceCell, #[property(get, set, construct_only)] id: Cell, #[property(get, set, construct_only)] name: OnceCell, #[property(get, set, construct_only)] title: OnceCell, #[property(get, set, construct_only)] is_folder: Cell, #[property(get)] pub bytes_completed: Cell, #[property(get, set, construct_only)] length: Cell, #[property(get, set = Self::set_wanted)] pub wanted: Cell, #[property(get)] wanted_inconsistent: Cell, #[property(get)] related: TrRelatedModel, related_wanted: RefCell>, related_length: RefCell>, related_bytes_completed: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TrFile { const NAME: &'static str = "TrFile"; type ParentType = glib::Object; type Type = super::TrFile; } #[glib::derived_properties] impl ObjectImpl for TrFile {} impl TrFile { fn set_wanted(&self, wanted: bool) { let fut = clone!( #[weak(rename_to = this)] self, async move { // Collect ids of files which shall get updated let ids = if this.obj().is_folder() { let path = this.obj().name(); // We can't use `related()` here, since that wouldn't return all nested folders / files let files = this.obj().torrent().files().related_files_by_path(&path); let mut ids = Vec::new(); for file in files { ids.push(file.id()); } ids } else { vec![this.obj().id()] }; if wanted { this.torrent .get() .unwrap() .set_wanted_files(ids) .await .unwrap(); } else { this.torrent .get() .unwrap() .set_unwanted_files(ids) .await .unwrap(); } this.wanted.set(wanted); this.obj().notify_wanted(); } ); glib::spawn_future_local(fut); } pub fn find_title(name: &str) -> String { if !name.contains('/') { return name.to_string(); } let slashes = name.match_indices('/'); name[(slashes.clone().last().unwrap().0) + 1..name.len()].to_string() } /// Update `self` when a related file changes its `wanted` state pub fn update_related_wanted(&self, related_file: &super::TrFile) { assert!(self.obj().is_folder()); if related_file.wanted() && !related_file.wanted_inconsistent() { self.related_wanted.borrow_mut().insert(related_file.name()); } else { self.related_wanted .borrow_mut() .remove(&related_file.name()); } // Check if this folder is in a inconsistent state // (mixed wanted/not wanted related files) let files_count = self.obj().related().n_items() as usize; let wanted_count = self.related_wanted.borrow().len(); if files_count == wanted_count { self.wanted.set(true); self.wanted_inconsistent.set(false); } else if wanted_count == 0 { self.wanted.set(false); self.wanted_inconsistent.set(false); } else { self.wanted.set(true); self.wanted_inconsistent.set(true); } self.obj().notify_wanted(); self.obj().notify_wanted_inconsistent(); } /// Update `self` when a related file changes its `length` state pub fn update_related_length(&self, related_file: &super::TrFile) { assert!(self.obj().is_folder()); let obj = self.obj(); let mut related_length = self.related_length.borrow_mut(); let mut value_changed = false; let mut previous_value: i64 = 0; if let Some(length) = related_length.get(&related_file.name()) { if length != &related_file.length() { value_changed = true; previous_value = *length; } } else { related_length.insert(related_file.name(), related_file.length()); self.length.set(obj.length() + related_file.length()); self.obj().notify_length(); } if value_changed { related_length.insert(related_file.name(), related_file.length()); self.length.set(obj.length() - previous_value); self.length.set(obj.length() + related_file.length()); self.obj().notify_length(); } } /// Update `self` when a related file changes its `bytes_completed` /// state pub fn update_related_bytes_completed(&self, related_file: &super::TrFile) { assert!(self.obj().is_folder()); let obj = self.obj(); let mut related_bytes_completed = self.related_bytes_completed.borrow_mut(); let mut value_changed = false; let mut previous_value: i64 = 0; if let Some(bytes_completed) = related_bytes_completed.get(&related_file.name()) { if bytes_completed != &related_file.bytes_completed() { value_changed = true; previous_value = *bytes_completed; } } else { related_bytes_completed.insert(related_file.name(), related_file.bytes_completed()); self.bytes_completed .set(obj.bytes_completed() + related_file.bytes_completed()); self.obj().notify_bytes_completed(); } if value_changed { related_bytes_completed.insert(related_file.name(), related_file.bytes_completed()); self.bytes_completed .set(obj.bytes_completed() - previous_value); self.bytes_completed .set(obj.bytes_completed() + related_file.bytes_completed()); self.obj().notify_bytes_completed(); } } } } glib::wrapper! { pub struct TrFile(ObjectSubclass); } impl TrFile { pub(crate) fn from_rpc_file(id: i32, rpc_file: &File, torrent: &TrTorrent) -> Self { let name = rpc_file.name.clone(); let title = imp::TrFile::find_title(&name); glib::Object::builder() .property("id", id) .property("name", &name) .property("title", &title) .property("length", rpc_file.length) .property("is-folder", false) .property("torrent", torrent) .build() } pub(crate) fn new_folder(name: &str, torrent: &TrTorrent) -> Self { let title = imp::TrFile::find_title(name); glib::Object::builder() .property("id", -1) .property("name", name) .property("title", &title) .property("is-folder", true) .property("torrent", torrent) .build() } /// Add a new related [TrFile] to `self` (which is a folder) pub(crate) fn add_related(&self, file: &TrFile) { assert!(self.is_folder()); let imp = self.imp(); self.related().add_file(file); file.connect_notify_local( Some("wanted"), clone!( #[weak(rename_to = this)] imp, move |file, _| { this.update_related_wanted(file); } ), ); file.connect_notify_local( Some("wanted-inconsistent"), clone!( #[weak(rename_to = this)] imp, move |file, _| { this.update_related_wanted(file); } ), ); imp.update_related_wanted(file); file.connect_notify_local( Some("length"), clone!( #[weak(rename_to = this)] imp, move |file, _| { this.update_related_length(file); } ), ); imp.update_related_length(file); file.connect_notify_local( Some("bytes-completed"), clone!( #[weak(rename_to = this)] imp, move |file, _| { this.update_related_bytes_completed(file); } ), ); imp.update_related_bytes_completed(file); } /// Updates the values of `self` (which is not a folder) pub(crate) fn refresh_values(&self, rpc_file_stat: &FileStat) { assert!(!self.is_folder()); let imp = self.imp(); // bytes_completed if imp.bytes_completed.get() != rpc_file_stat.bytes_completed { imp.bytes_completed.set(rpc_file_stat.bytes_completed); self.notify_bytes_completed(); } // wanted if imp.wanted.get() != rpc_file_stat.wanted { imp.wanted.set(rpc_file_stat.wanted); self.notify_wanted(); } } } transmission-gobject-0.1.6/src/file_model.rs000064400000000000000000000144031046102023000172320ustar 00000000000000use std::cell::{Cell, RefCell}; use gio::prelude::*; use gio::subclass::prelude::*; use glib::Properties; use indexmap::map::IndexMap; use transmission_client::TorrentFiles; use crate::{TrFile, TrTorrent}; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrFileModel)] pub struct TrFileModel { /// The top level file #[property(get, nullable)] pub top_level: RefCell>, /// Whether this contains data, and can be consumed #[property(get)] pub is_ready: Cell, /// All files and folders (Full path + TrFile) pub map: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TrFileModel { const NAME: &'static str = "TrFileModel"; type ParentType = glib::Object; type Type = super::TrFileModel; type Interfaces = (gio::ListModel,); } #[glib::derived_properties] impl ObjectImpl for TrFileModel {} impl ListModelImpl for TrFileModel { fn item_type(&self) -> glib::Type { TrFile::static_type() } fn n_items(&self) -> u32 { self.map.borrow().len() as u32 } fn item(&self, position: u32) -> Option { self.map .borrow() .get_index(position.try_into().unwrap()) .map(|(_, o)| o.clone().upcast::()) } } impl TrFileModel { pub fn add_file(&self, file: &TrFile, torrent: &TrTorrent) { if self.obj().file_by_name(&file.name()).is_some() { warn!("File {:?} alis_ready exists in model", file.name()); return; } let mut map = self.map.borrow_mut(); let full_path = file.name(); // Resolve individual folders to make sure that they're added as TrFile to the // model eg. "there/can/be/nested/folders/file.txt" // -> there, can, be, nested, folders let mut folder_names = Vec::new(); let folder_name = if full_path.contains('/') { let slashes = full_path.match_indices('/'); let folder_name = full_path[..slashes.clone().last().unwrap().0].to_string(); for slash in slashes { let folder_name = full_path[..slash.0].to_string(); folder_names.push(folder_name); } Some(folder_name) } else { // No folders, it's a single file torrent self.top_level.borrow_mut().replace(file.clone()); self.obj().notify_top_level(); None }; // Make sure that nested folders are related to their parent let mut parent: Option = None; for folder_name in folder_names { let folder = if let Some(folder) = map.get(&folder_name) { folder.clone() } else { let folder = TrFile::new_folder(&folder_name, torrent); // We know that the folder is related / a subfolder of the parent if let Some(parent) = parent { parent.add_related(&folder); } else { // No parent -> the current file is the toplevel folder self.top_level.borrow_mut().replace(folder.clone()); self.obj().notify_top_level(); } map.insert(folder_name.clone(), folder.clone()); folder }; parent = Some(folder); } // Now since we made sure that all folders are added, // we can take care of the actual file map.insert(full_path.clone(), file.clone()); let pos = (map.len() - 1) as u32; self.obj().items_changed(pos, 0, 1); if let Some(folder_name) = folder_name { // Get the actual folder to add the file as related file (child) map.get_mut(&folder_name).unwrap().add_related(file); } } } } glib::wrapper! { pub struct TrFileModel(ObjectSubclass) @implements gio::ListModel; } impl TrFileModel { pub(crate) fn refresh_files(&self, rpc_files: &TorrentFiles, torrent: &TrTorrent) { let is_initial = self.top_level().is_none(); for (index, rpc_file) in rpc_files.files.iter().enumerate() { let rpc_file_stat = rpc_files.file_stats.get(index).cloned().unwrap_or_default(); if let Some(file) = self.file_by_name(&rpc_file.name) { file.refresh_values(&rpc_file_stat); } else { let file = TrFile::from_rpc_file(index.try_into().unwrap(), rpc_file, torrent); file.refresh_values(&rpc_file_stat); self.imp().add_file(&file, torrent); } } if is_initial && self.top_level().is_some() { self.imp().is_ready.set(true); self.notify_is_ready(); } } pub(crate) fn related_files_by_path(&self, path: &str) -> Vec { let imp = self.imp(); let mut result = Vec::new(); for (file_path, file) in &*imp.map.borrow() { if file_path.contains(path) && !file.is_folder() { result.push(file.clone()); } } result } /// Returns a [TrFile] based on its name (path) pub fn file_by_name(&self, name: &str) -> Option { self.imp() .map .borrow() .get(name) .map(|o| o.clone().downcast().unwrap()) } /// Returns parent folder for [TrFile] pub fn parent(&self, folder: &TrFile) -> Option { let folder_name = folder.name(); if folder_name.contains('/') { let slashes = folder_name.match_indices('/'); let parent_name = folder.name()[0..slashes.clone().last().unwrap().0].to_string(); let parent = self.file_by_name(&parent_name); return parent; } None } } impl Default for TrFileModel { fn default() -> Self { glib::Object::new() } } transmission-gobject-0.1.6/src/lib.rs000064400000000000000000000013001046102023000156710ustar 00000000000000#[macro_use] extern crate log; mod authentication; mod client; mod encryption; mod file; mod file_model; mod related_model; mod session; mod session_stats; mod session_stats_details; mod torrent; mod torrent_model; mod torrent_status; pub use authentication::TrAuthentication; pub use client::TrClient; pub use encryption::TrEncryption; pub use file::TrFile; pub use file_model::TrFileModel; pub use related_model::TrRelatedModel; pub use session::TrSession; pub use session_stats::TrSessionStats; pub use session_stats_details::TrSessionStatsDetails; pub use torrent::TrTorrent; pub use torrent_model::TrTorrentModel; pub use torrent_status::TrTorrentStatus; pub use transmission_client::ClientError; transmission-gobject-0.1.6/src/related_model.rs000064400000000000000000000035131046102023000177330ustar 00000000000000use std::cell::RefCell; use gio::prelude::*; use gio::subclass::prelude::*; use indexmap::map::IndexMap; use crate::TrFile; mod imp { use super::*; #[derive(Debug, Default)] pub struct TrRelatedModel { pub map: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TrRelatedModel { const NAME: &'static str = "TrRelatedModel"; type Type = super::TrRelatedModel; type Interfaces = (gio::ListModel,); } impl ObjectImpl for TrRelatedModel {} impl ListModelImpl for TrRelatedModel { fn item_type(&self) -> glib::Type { TrFile::static_type() } fn n_items(&self) -> u32 { self.map.borrow().len() as u32 } fn item(&self, position: u32) -> Option { self.map .borrow() .get_index(position.try_into().unwrap()) .map(|(_, o)| o.clone().upcast::()) } } impl TrRelatedModel {} } glib::wrapper! { pub struct TrRelatedModel(ObjectSubclass) @implements gio::ListModel; } impl TrRelatedModel { pub fn new() -> Self { glib::Object::new() } pub(crate) fn add_file(&self, file: &TrFile) { let pos = { let mut map = self.imp().map.borrow_mut(); if map.contains_key(&file.name()) { warn!( "Model already contains file {} with name {}", file.title(), file.name() ); return; } map.insert(file.name(), file.clone()); (map.len() - 1) as u32 }; self.items_changed(pos, 0, 1); } } impl Default for TrRelatedModel { fn default() -> Self { Self::new() } } transmission-gobject-0.1.6/src/session.rs000064400000000000000000000273151046102023000166240ustar 00000000000000use std::cell::{Cell, OnceCell, RefCell}; use gio::prelude::FileExt; use glib::object::ObjectExt; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::{clone, Properties}; use transmission_client::{Session, SessionMutator}; use crate::{TrClient, TrEncryption}; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrSession)] pub struct TrSession { #[property(get, set, construct_only)] pub client: OnceCell, #[property(get)] pub version: RefCell, #[property(get, set = Self::set_download_dir)] pub download_dir: RefCell>, #[property(get, set = Self::set_start_added_torrents)] pub start_added_torrents: Cell, #[property(get, set = Self::set_encryption, builder(Default::default()))] pub encryption: Cell, #[property(get, set = Self::set_incomplete_dir_enabled)] pub incomplete_dir_enabled: Cell, #[property(get, set = Self::set_incomplete_dir)] pub incomplete_dir: RefCell>, #[property(get, set = Self::set_download_queue_enabled)] pub download_queue_enabled: Cell, #[property(get, set = Self::set_download_queue_size, minimum = 1, default_value = 1)] pub download_queue_size: Cell, #[property(get, set = Self::set_seed_queue_enabled)] pub seed_queue_enabled: Cell, #[property(get, set = Self::set_seed_queue_size, minimum = 1, default_value = 1)] pub seed_queue_size: Cell, #[property(get, set = Self::set_port_forwarding_enabled)] pub port_forwarding_enabled: Cell, #[property(get, set = Self::set_peer_port_random_on_start)] pub peer_port_random_on_start: Cell, #[property(get, set = Self::set_peer_port, minimum = 1, default_value = 1)] pub peer_port: Cell, #[property(get, set = Self::set_peer_limit_global, minimum = 1, default_value = 1)] pub peer_limit_global: Cell, #[property(get, set = Self::set_peer_limit_per_torrent, minimum = 1, default_value = 1)] pub peer_limit_per_torrent: Cell, } #[glib::object_subclass] impl ObjectSubclass for TrSession { const NAME: &'static str = "TrSession"; type ParentType = glib::Object; type Type = super::TrSession; } #[glib::derived_properties] impl ObjectImpl for TrSession {} impl TrSession { pub fn set_download_dir(&self, value: gio::File) { *self.download_dir.borrow_mut() = Some(value.clone()); let mutator = SessionMutator { download_dir: Some(value.path().unwrap()), ..Default::default() }; self.mutate_session(mutator, "download_dir"); } pub fn set_start_added_torrents(&self, value: bool) { self.start_added_torrents.set(value); let mutator = SessionMutator { start_added_torrents: Some(value), ..Default::default() }; self.mutate_session(mutator, "start-added-torrents"); } pub fn set_encryption(&self, value: TrEncryption) { self.encryption.set(value); let mutator = SessionMutator { encryption: Some(value.into()), ..Default::default() }; self.mutate_session(mutator, "encryption"); } pub fn set_incomplete_dir_enabled(&self, value: bool) { self.incomplete_dir_enabled.set(value); let mutator = SessionMutator { incomplete_dir_enabled: Some(value), ..Default::default() }; self.mutate_session(mutator, "incomplete-dir-enabled"); } pub fn set_incomplete_dir(&self, value: gio::File) { *self.incomplete_dir.borrow_mut() = Some(value.clone()); let mutator = SessionMutator { incomplete_dir: Some(value.path().unwrap()), ..Default::default() }; self.mutate_session(mutator, "incomplete-dir"); } pub fn set_download_queue_enabled(&self, value: bool) { self.download_queue_enabled.set(value); let mutator = SessionMutator { download_queue_enabled: Some(value), ..Default::default() }; self.mutate_session(mutator, "download-queue-enabled"); } pub fn set_download_queue_size(&self, value: i32) { self.download_queue_size.set(value); let mutator = SessionMutator { download_queue_size: Some(value), ..Default::default() }; self.mutate_session(mutator, "download-queue-size"); } pub fn set_seed_queue_enabled(&self, value: bool) { self.seed_queue_enabled.set(value); let mutator = SessionMutator { seed_queue_enabled: Some(value), ..Default::default() }; self.mutate_session(mutator, "seed-queue-enabled"); } pub fn set_seed_queue_size(&self, value: i32) { self.seed_queue_size.set(value); let mutator = SessionMutator { seed_queue_size: Some(value), ..Default::default() }; self.mutate_session(mutator, "seed-queue-size"); } pub fn set_port_forwarding_enabled(&self, value: bool) { self.port_forwarding_enabled.set(value); let mutator = SessionMutator { port_forwarding_enabled: Some(value), ..Default::default() }; self.mutate_session(mutator, "port-forwarding-enabled"); } pub fn set_peer_port_random_on_start(&self, value: bool) { self.peer_port_random_on_start.set(value); let mutator = SessionMutator { peer_port_random_on_start: Some(value), ..Default::default() }; self.mutate_session(mutator, "peer-port-random-on-start"); } pub fn set_peer_port(&self, value: i32) { self.peer_port.set(value); let mutator = SessionMutator { peer_port: Some(value), ..Default::default() }; self.mutate_session(mutator, "peer-port"); } pub fn set_peer_limit_global(&self, value: i32) { self.peer_limit_global.set(value); let mutator = SessionMutator { peer_limit_global: Some(value), ..Default::default() }; self.mutate_session(mutator, "peer-limit-global"); } pub fn set_peer_limit_per_torrent(&self, value: i32) { self.peer_limit_per_torrent.set(value); let mutator = SessionMutator { peer_limit_per_torrent: Some(value), ..Default::default() }; self.mutate_session(mutator, "peer-limit-per-torrent"); } fn mutate_session(&self, mutator: SessionMutator, prop_name: &str) { self.obj().notify(prop_name); let fut = clone!( #[weak(rename_to = this)] self, async move { if let Some(rpc_client) = this.client.get().unwrap().rpc_client() { rpc_client.session_set(mutator).await.unwrap(); } else { warn!("Unable set mutate session, no rpc connection."); } } ); glib::spawn_future_local(fut); } } } glib::wrapper! { pub struct TrSession(ObjectSubclass); } impl TrSession { pub(crate) fn new(client: &TrClient) -> Self { glib::Object::builder().property("client", client).build() } pub(crate) fn refresh_values(&self, rpc_session: Session) { let imp = self.imp(); // version if *imp.version.borrow() != rpc_session.version { *imp.version.borrow_mut() = rpc_session.version; self.notify_version(); } // download_dir let download_dir = gio::File::for_path(rpc_session.download_dir); if download_dir.path() != imp.download_dir.borrow().as_ref().and_then(|f| f.path()) { *imp.download_dir.borrow_mut() = Some(download_dir); self.notify_download_dir(); } // start_added_torrents if imp.start_added_torrents.get() != rpc_session.start_added_torrents { imp.start_added_torrents .set(rpc_session.start_added_torrents); self.notify_start_added_torrents(); } // encryption if imp.encryption.get() != rpc_session.encryption.clone().into() { imp.encryption.set(rpc_session.encryption.into()); self.notify_encryption(); } // incomplete_dir_enabled if imp.incomplete_dir_enabled.get() != rpc_session.incomplete_dir_enabled { imp.incomplete_dir_enabled .set(rpc_session.incomplete_dir_enabled); self.notify_incomplete_dir_enabled(); } // incomplete_dir let incomplete_dir = gio::File::for_path(rpc_session.incomplete_dir); if incomplete_dir.path() != imp.incomplete_dir.borrow().as_ref().and_then(|f| f.path()) { *imp.incomplete_dir.borrow_mut() = Some(incomplete_dir); self.notify_incomplete_dir(); } // download_queue_enabled if imp.download_queue_enabled.get() != rpc_session.download_queue_enabled { imp.download_queue_enabled .set(rpc_session.download_queue_enabled); self.notify_download_queue_enabled(); } // download_queue_size if imp.download_queue_size.get() != rpc_session.download_queue_size { imp.download_queue_size.set(rpc_session.download_queue_size); self.notify_download_queue_size(); } // seed_queue_enabled if imp.seed_queue_enabled.get() != rpc_session.seed_queue_enabled { imp.seed_queue_enabled.set(rpc_session.seed_queue_enabled); self.notify_seed_queue_enabled(); } // seed_queue_size if imp.seed_queue_size.get() != rpc_session.seed_queue_size { imp.seed_queue_size.set(rpc_session.seed_queue_size); self.notify_seed_queue_size(); } // port_forwarding_enabled if imp.port_forwarding_enabled.get() != rpc_session.port_forwarding_enabled { imp.port_forwarding_enabled .set(rpc_session.port_forwarding_enabled); self.notify_port_forwarding_enabled(); } // peer_port_random_on_start if imp.peer_port_random_on_start.get() != rpc_session.peer_port_random_on_start { imp.peer_port_random_on_start .set(rpc_session.peer_port_random_on_start); self.notify_peer_port_random_on_start(); } // peer_port if imp.peer_port.get() != rpc_session.peer_port { imp.peer_port.set(rpc_session.peer_port); self.notify_peer_port(); } // peer_limit_global if imp.peer_limit_global.get() != rpc_session.peer_limit_global { imp.peer_limit_global.set(rpc_session.peer_limit_global); self.notify_peer_limit_global(); } // peer_limit_per_torrent if imp.peer_limit_per_torrent.get() != rpc_session.peer_limit_per_torrent { imp.peer_limit_per_torrent .set(rpc_session.peer_limit_per_torrent); self.notify_peer_limit_per_torrent(); } } } transmission-gobject-0.1.6/src/session_stats.rs000064400000000000000000000062131046102023000200340ustar 00000000000000use std::cell::Cell; use glib::object::ObjectExt; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::Properties; use transmission_client::SessionStats; use crate::TrSessionStatsDetails; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrSessionStats)] pub struct TrSessionStats { #[property(get, minimum = 1, default_value = 1)] pub torrent_count: Cell, #[property(get, minimum = 1, default_value = 1)] pub active_torrent_count: Cell, #[property(get, minimum = 1, default_value = 1)] pub paused_torrent_count: Cell, #[property(get, minimum = 1, default_value = 1)] pub downloaded_torrent_count: Cell, #[property(get, minimum = 1, default_value = 1)] pub download_speed: Cell, #[property(get, minimum = 1, default_value = 1)] pub upload_speed: Cell, #[property(get)] pub cumulative_stats: TrSessionStatsDetails, #[property(get)] pub current_stats: TrSessionStatsDetails, } #[glib::object_subclass] impl ObjectSubclass for TrSessionStats { const NAME: &'static str = "TrSessionStats"; type ParentType = glib::Object; type Type = super::TrSessionStats; } #[glib::derived_properties] impl ObjectImpl for TrSessionStats {} } glib::wrapper! { pub struct TrSessionStats(ObjectSubclass); } impl TrSessionStats { pub(crate) fn refresh_values(&self, rpc_stats: SessionStats, download_count: i32) { let imp = self.imp(); // torrent_count if imp.torrent_count.get() != rpc_stats.torrent_count { imp.torrent_count.set(rpc_stats.torrent_count); self.notify_torrent_count(); } // active_torrent_count if imp.active_torrent_count.get() != rpc_stats.active_torrent_count { imp.active_torrent_count.set(rpc_stats.active_torrent_count); self.notify_active_torrent_count(); } // paused_torrent_count if imp.paused_torrent_count.get() != rpc_stats.paused_torrent_count { imp.paused_torrent_count.set(rpc_stats.paused_torrent_count); self.notify_paused_torrent_count(); } // downloaded_torrent_count if imp.downloaded_torrent_count.get() != download_count { imp.downloaded_torrent_count.set(download_count); self.notify_downloaded_torrent_count(); } // download_speed if imp.download_speed.get() != rpc_stats.download_speed { imp.download_speed.set(rpc_stats.download_speed); self.notify_download_speed(); } // upload_speed if imp.upload_speed.get() != rpc_stats.upload_speed { imp.upload_speed.set(rpc_stats.upload_speed); self.notify_upload_speed(); } imp.cumulative_stats .refresh_values(rpc_stats.cumulative_stats); imp.current_stats.refresh_values(rpc_stats.current_stats); } } impl Default for TrSessionStats { fn default() -> Self { glib::Object::new() } } transmission-gobject-0.1.6/src/session_stats_details.rs000064400000000000000000000045011046102023000215370ustar 00000000000000use std::cell::Cell; use glib::object::ObjectExt; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::Properties; use transmission_client::StatsDetails; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrSessionStatsDetails)] pub struct TrSessionStatsDetails { #[property(get)] pub seconds_active: Cell, #[property(get)] pub downloaded_bytes: Cell, #[property(get)] pub uploaded_bytes: Cell, #[property(get)] pub files_added: Cell, #[property(get)] pub session_count: Cell, } #[glib::object_subclass] impl ObjectSubclass for TrSessionStatsDetails { const NAME: &'static str = "TrSessionStatsDetails"; type ParentType = glib::Object; type Type = super::TrSessionStatsDetails; } #[glib::derived_properties] impl ObjectImpl for TrSessionStatsDetails {} } glib::wrapper! { pub struct TrSessionStatsDetails(ObjectSubclass); } impl TrSessionStatsDetails { pub(crate) fn refresh_values(&self, rpc_details: StatsDetails) { let imp = self.imp(); // seconds_active if imp.seconds_active.get() != rpc_details.seconds_active { imp.seconds_active.set(rpc_details.seconds_active); self.notify_seconds_active(); } // downloaded_bytes if imp.downloaded_bytes.get() != rpc_details.downloaded_bytes { imp.downloaded_bytes.set(rpc_details.downloaded_bytes); self.notify_downloaded_bytes(); } // uploaded_bytes if imp.uploaded_bytes.get() != rpc_details.uploaded_bytes { imp.uploaded_bytes.set(rpc_details.uploaded_bytes); self.notify_uploaded_bytes(); } // files_added if imp.files_added.get() != rpc_details.files_added { imp.files_added.set(rpc_details.files_added); self.notify_files_added(); } // session_count if imp.session_count.get() != rpc_details.session_count { imp.session_count.set(rpc_details.session_count); self.notify_session_count(); } } } impl Default for TrSessionStatsDetails { fn default() -> Self { glib::Object::new() } } transmission-gobject-0.1.6/src/torrent.rs000064400000000000000000000323531046102023000166340ustar 00000000000000use std::cell::{OnceCell, RefCell}; use std::time::Duration; use gio::prelude::*; use gio::File; use glib::subclass::prelude::{ObjectSubclass, *}; use glib::{clone, Properties}; use transmission_client::{ClientError, Torrent, TorrentFiles, TorrentMutator, TorrentPeers}; use crate::{TrClient, TrFileModel, TrTorrentStatus}; mod imp { use super::*; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::TrTorrent)] pub struct TrTorrent { // Static values #[property(get, set, construct_only)] pub name: OnceCell, #[property(get, set, construct_only)] pub hash: OnceCell, #[property(get, set, construct_only)] pub primary_mime_type: OnceCell, #[property(get, set, construct_only)] pub magnet_link: OnceCell, #[property(get, set, construct_only)] pub client: OnceCell, // Dynamic values #[property(get)] pub download_dir: RefCell, #[property(get)] pub size: RefCell, #[property(get, builder(Default::default()))] pub status: RefCell, #[property(get)] pub is_stalled: RefCell, #[property(get)] pub eta: RefCell, #[property(get)] pub error: RefCell, #[property(get)] pub error_string: RefCell, #[property(get)] pub progress: RefCell, #[property(get)] pub queue_position: RefCell, #[property(get)] pub download_queue_position: RefCell, #[property(get)] pub seed_queue_position: RefCell, #[property(get)] pub seeders_active: RefCell, #[property(get)] pub seeders: RefCell, #[property(get)] pub leechers: RefCell, #[property(get)] pub downloaded: RefCell, #[property(get)] pub uploaded: RefCell, #[property(get)] pub download_speed: RefCell, #[property(get)] pub upload_speed: RefCell, #[property(get)] pub metadata_percent_complete: RefCell, #[property(get)] pub files: TrFileModel, /// Whether to poll extra info like files or available peers, which data /// can be pretty expensive to deserialize #[property(get, set = Self::set_update_extra_info)] pub update_extra_info: RefCell, } #[glib::object_subclass] impl ObjectSubclass for TrTorrent { const NAME: &'static str = "TrTorrent"; type ParentType = glib::Object; type Type = super::TrTorrent; } #[glib::derived_properties] impl ObjectImpl for TrTorrent {} impl TrTorrent { fn set_update_extra_info(&self, value: bool) { self.update_extra_info.set(value); self.obj().notify_update_extra_info(); let fut = clone!( #[weak(rename_to = this)] self, async move { this.do_poll().await; } ); glib::spawn_future_local(fut); } pub async fn do_poll(&self) { self.obj().client().refresh_data().await; } } } glib::wrapper! { pub struct TrTorrent(ObjectSubclass); } impl TrTorrent { pub fn from_rpc_torrent(rpc_torrent: &Torrent, client: &TrClient) -> Self { glib::Object::builder() .property("hash", &rpc_torrent.hash_string) .property("name", &rpc_torrent.name) .property("primary-mime-type", &rpc_torrent.primary_mime_type) .property("magnet-link", &rpc_torrent.magnet_link) .property("client", client) .build() } pub async fn start(&self, bypass_queue: bool) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { client .torrent_start(Some(vec![self.hash()]), bypass_queue) .await?; self.imp().do_poll().await; } else { warn!("Unable to start torrent, no rpc connection."); } Ok(()) } pub async fn stop(&self) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { client.torrent_stop(Some(vec![self.hash()])).await?; // The daemon needs a short time to stop the torrent glib::timeout_future(Duration::from_millis(500)).await; self.imp().do_poll().await; } else { warn!("Unable to stop torrent, no rpc connection."); } Ok(()) } pub async fn remove(&self, delete_local_data: bool) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { client .torrent_remove(Some(vec![self.hash()]), delete_local_data) .await?; self.imp().do_poll().await; } else { warn!("Unable to stop torrent, no rpc connection."); } Ok(()) } pub async fn reannounce(&self) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { client.torrent_reannounce(Some(vec![self.hash()])).await?; self.imp().do_poll().await; } else { warn!("Unable to stop torrent, no rpc connection."); } Ok(()) } pub async fn set_location(&self, location: File, move_data: bool) -> Result<(), ClientError> { let path = location.path().unwrap().to_str().unwrap().to_string(); if let Some(client) = self.client().rpc_client() { client .torrent_set_location(Some(vec![self.hash()]), path, move_data) .await?; self.imp().do_poll().await; } else { warn!("Unable to set torrent location, no rpc connection."); } Ok(()) } pub(crate) async fn set_wanted_files(&self, wanted: Vec) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { let mutator = TorrentMutator { files_wanted: Some(wanted), ..Default::default() }; client.torrent_set(Some(vec![self.hash()]), mutator).await?; } else { warn!("Unable to update files, no rpc connection."); } Ok(()) } pub(crate) async fn set_unwanted_files(&self, wanted: Vec) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { let mutator = TorrentMutator { files_unwanted: Some(wanted), ..Default::default() }; client.torrent_set(Some(vec![self.hash()]), mutator).await?; } else { warn!("Unable to update files, no rpc connection."); } Ok(()) } pub(crate) fn refresh_values( &self, rpc_torrent: &Torrent, rpc_files: &TorrentFiles, rpc_peers: &TorrentPeers, ) { let imp = self.imp(); // download_dir if *imp.download_dir.borrow() != rpc_torrent.download_dir { imp.download_dir .borrow_mut() .clone_from(&rpc_torrent.download_dir); self.notify_download_dir(); } // size if *imp.size.borrow() != rpc_torrent.size_when_done { *imp.size.borrow_mut() = rpc_torrent.size_when_done; self.notify_size(); } // status if *imp.status.borrow() as u32 != rpc_torrent.status as u32 { let status = TrTorrentStatus::try_from(rpc_torrent.status as u32).unwrap(); // Check if status changed from "Downloading" to "Seeding" // and emit TrClient `torrent-downloaded` signal if self.status() == TrTorrentStatus::Download && (status == TrTorrentStatus::SeedWait || status == TrTorrentStatus::Seed) { imp.client .get() .unwrap() .emit_by_name::<()>("torrent-downloaded", &[&self]); } *imp.status.borrow_mut() = status; self.notify_status(); } // is_stalled if *imp.is_stalled.borrow() as u32 != rpc_torrent.is_stalled as u32 { *imp.is_stalled.borrow_mut() = rpc_torrent.is_stalled; self.notify_is_stalled(); } // eta if *imp.eta.borrow() != rpc_torrent.eta { *imp.eta.borrow_mut() = rpc_torrent.eta; self.notify_eta(); } // error if *imp.error.borrow() != rpc_torrent.error { *imp.error.borrow_mut() = rpc_torrent.error; self.notify_error(); } // error-string if *imp.error_string.borrow() != rpc_torrent.error_string { imp.error_string .borrow_mut() .clone_from(&rpc_torrent.error_string); self.notify_error_string(); } // progress if *imp.progress.borrow() != rpc_torrent.percent_done { *imp.progress.borrow_mut() = rpc_torrent.percent_done; self.notify_progress(); } // queue_position if *imp.queue_position.borrow() != rpc_torrent.queue_position { *imp.queue_position.borrow_mut() = rpc_torrent.queue_position; self.notify_queue_position(); } // downloaded if *imp.downloaded.borrow() != rpc_torrent.have_valid { *imp.downloaded.borrow_mut() = rpc_torrent.have_valid; self.notify_downloaded(); } // uploaded if *imp.uploaded.borrow() != rpc_torrent.uploaded_ever { *imp.uploaded.borrow_mut() = rpc_torrent.uploaded_ever; self.notify_uploaded(); } // downloaded if *imp.download_speed.borrow() != rpc_torrent.rate_download { *imp.download_speed.borrow_mut() = rpc_torrent.rate_download; self.notify_download_speed(); } // uploaded if *imp.upload_speed.borrow() != rpc_torrent.rate_upload { *imp.upload_speed.borrow_mut() = rpc_torrent.rate_upload; self.notify_upload_speed(); } // metadata_percent_complete if *imp.metadata_percent_complete.borrow() != rpc_torrent.metadata_percent_complete { *imp.metadata_percent_complete.borrow_mut() = rpc_torrent.metadata_percent_complete; self.notify_metadata_percent_complete(); } // Torrent peers // seeders_active if *imp.seeders_active.borrow() != rpc_peers.peers_sending_to_us { *imp.seeders_active.borrow_mut() = rpc_peers.peers_sending_to_us; self.notify("seeders_active"); } // seeders if *imp.seeders.borrow() != rpc_peers.peers_connected { *imp.seeders.borrow_mut() = rpc_peers.peers_connected; self.notify("seeders"); } // leechers if *imp.leechers.borrow() != rpc_peers.peers_getting_from_us { *imp.leechers.borrow_mut() = rpc_peers.peers_getting_from_us; self.notify("leechers"); } // Torrent files self.files().refresh_files(rpc_files, self); } pub(crate) fn refresh_queue_positions(&self, download_queue_pos: i32, seed_queue_pos: i32) { let imp = self.imp(); // download_queue_position if *imp.download_queue_position.borrow() != download_queue_pos { *imp.download_queue_position.borrow_mut() = download_queue_pos; self.notify_download_queue_position(); } // seed_queue_position if *imp.seed_queue_position.borrow() != seed_queue_pos { *imp.seed_queue_position.borrow_mut() = seed_queue_pos; self.notify_seed_queue_position(); } } pub async fn set_queue_position(&self, pos: i32) -> Result<(), ClientError> { if let Some(client) = self.client().rpc_client() { let mutator = TorrentMutator { queue_position: Some(pos), ..Default::default() }; client.torrent_set(Some(vec![self.hash()]), mutator).await?; } else { warn!("Unable set queue position, no rpc connection."); } Ok(()) } pub async fn set_download_queue_position(&self, pos: i32) -> Result<(), ClientError> { let torrents = self.client().torrents(); for i in 0..torrents.n_items() { let torrent: TrTorrent = torrents.item(i).unwrap().downcast().unwrap(); if torrent.download_queue_position() == pos { self.set_queue_position(torrent.queue_position()).await?; break; } } self.imp().do_poll().await; Ok(()) } pub async fn set_seed_queue_position(&self, pos: i32) -> Result<(), ClientError> { let torrents = self.client().torrents(); for i in 0..torrents.n_items() { let torrent: TrTorrent = torrents.item(i).unwrap().downcast().unwrap(); if torrent.seed_queue_position() == pos { self.set_queue_position(torrent.queue_position()).await?; break; } } self.imp().do_poll().await; Ok(()) } } transmission-gobject-0.1.6/src/torrent_model.rs000064400000000000000000000070461046102023000200150ustar 00000000000000use gio::prelude::*; use gio::subclass::prelude::*; use glib::subclass::Signal; use once_cell::sync::Lazy; use crate::torrent::TrTorrent; mod imp { use std::cell::RefCell; use super::*; #[derive(Debug, Default)] pub struct TrTorrentModel { pub vec: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TrTorrentModel { const NAME: &'static str = "TrTorrentModel"; type ParentType = glib::Object; type Type = super::TrTorrentModel; type Interfaces = (gio::ListModel,); } impl ObjectImpl for TrTorrentModel { fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![Signal::builder("status-changed") .flags(glib::SignalFlags::ACTION) .build()] }); SIGNALS.as_ref() } } impl ListModelImpl for TrTorrentModel { fn item_type(&self) -> glib::Type { TrTorrent::static_type() } fn n_items(&self) -> u32 { self.vec.borrow().len() as u32 } fn item(&self, position: u32) -> Option { self.vec .borrow() .get(position as usize) .map(|o| o.clone().upcast::()) } } impl TrTorrentModel { pub fn find(&self, hash: String) -> Option { for pos in 0..self.obj().n_items() { let obj = self.obj().item(pos)?; let s = obj.downcast::().unwrap(); if s.hash() == hash { return Some(pos); } } None } } } glib::wrapper! { pub struct TrTorrentModel(ObjectSubclass) @implements gio::ListModel; } impl TrTorrentModel { pub(crate) fn add_torrent(&self, torrent: &TrTorrent) { let imp = self.imp(); if self.imp().find(torrent.hash()).is_some() { warn!("Torrent {:?} already exists in model", torrent.name()); return; } // Own scope to avoid "already mutably borrowed: BorrowError" let pos = { let mut data = imp.vec.borrow_mut(); data.push(torrent.clone()); (data.len() - 1) as u32 }; self.items_changed(pos, 0, 1); } pub(crate) fn remove_torrent(&self, torrent: &TrTorrent) { let imp = self.imp(); match self.imp().find(torrent.hash()) { Some(pos) => { imp.vec.borrow_mut().remove(pos as usize); self.items_changed(pos, 1, 0); } None => warn!("Torrent {:?} not found in model", torrent.name()), } } pub fn torrent_by_hash(&self, hash: String) -> Option { if let Some(index) = self.imp().find(hash) { return Some(self.item(index).unwrap().downcast().unwrap()); } None } pub(crate) fn clear(&self) { let imp = self.imp(); let len = self.n_items(); imp.vec.borrow_mut().clear(); self.items_changed(0, len, 0); } pub(crate) fn get_hashes(&self) -> Vec { let mut hashes = Vec::new(); for pos in 0..self.n_items() { let obj = self.item(pos).unwrap(); let s = obj.downcast::().unwrap(); hashes.insert(0, s.hash()); } hashes } } impl Default for TrTorrentModel { fn default() -> Self { glib::Object::new() } } transmission-gobject-0.1.6/src/torrent_status.rs000064400000000000000000000005261046102023000202340ustar 00000000000000use glib::Enum; use num_enum::TryFromPrimitive; #[derive(Default, Debug, Copy, Clone, Enum, PartialEq, TryFromPrimitive)] #[repr(u32)] #[enum_type(name = "TrTorrentStatus")] pub enum TrTorrentStatus { #[default] Stopped = 0, CheckWait = 1, Check = 2, DownloadWait = 3, Download = 4, SeedWait = 5, Seed = 6, }