1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
5use tokio::sync::RwLock;
6use tracing::{debug, info};
7
8use crate::error::Result;
9use crate::rate_limiter::RateLimiter;
10use crate::segment::Segment;
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum DownloadStatus {
14 #[default]
15 Waiting,
16 Active,
17 Paused,
18 Error(String),
19 Complete,
20 Removed,
21}
22
23impl Serialize for DownloadStatus {
28 fn serialize<S: serde::Serializer>(
29 &self,
30 serializer: S,
31 ) -> std::result::Result<S::Ok, S::Error> {
32 serializer.serialize_str(self.as_str())
33 }
34}
35
36impl<'de> Deserialize<'de> for DownloadStatus {
37 fn deserialize<D: serde::Deserializer<'de>>(
38 deserializer: D,
39 ) -> std::result::Result<Self, D::Error> {
40 let s = String::deserialize(deserializer)?;
41 match s.as_str() {
42 "waiting" => Ok(DownloadStatus::Waiting),
43 "active" => Ok(DownloadStatus::Active),
44 "paused" => Ok(DownloadStatus::Paused),
45 "error" => Ok(DownloadStatus::Error(String::new())),
46 "complete" => Ok(DownloadStatus::Complete),
47 "removed" => Ok(DownloadStatus::Removed),
48 _ => Err(serde::de::Error::unknown_variant(
49 "DownloadStatus",
50 &[
51 "waiting", "active", "paused", "error", "complete", "removed",
52 ],
53 )),
54 }
55 }
56}
57
58impl fmt::Display for DownloadStatus {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 DownloadStatus::Waiting => write!(f, "waiting"),
62 DownloadStatus::Active => write!(f, "active"),
63 DownloadStatus::Paused => write!(f, "paused"),
64 DownloadStatus::Error(_) => write!(f, "error"),
65 DownloadStatus::Complete => write!(f, "complete"),
66 DownloadStatus::Removed => write!(f, "removed"),
67 }
68 }
69}
70
71impl DownloadStatus {
72 pub fn is_active(&self) -> bool {
73 matches!(self, DownloadStatus::Active | DownloadStatus::Waiting)
74 }
75
76 pub fn is_completed(&self) -> bool {
77 matches!(self, DownloadStatus::Complete)
78 }
79
80 pub fn is_paused(&self) -> bool {
81 matches!(self, DownloadStatus::Paused)
82 }
83
84 pub fn is_stopped(&self) -> bool {
85 !self.is_active() && !matches!(self, DownloadStatus::Removed)
86 }
87
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 DownloadStatus::Active => "active",
91 DownloadStatus::Waiting => "waiting",
92 DownloadStatus::Paused => "paused",
93 DownloadStatus::Error(_) => "error",
94 DownloadStatus::Complete => "complete",
95 DownloadStatus::Removed => "removed",
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct GroupId(pub u64);
102
103impl GroupId {
104 pub fn new(id: u64) -> Self {
105 GroupId(id)
106 }
107
108 pub fn value(&self) -> u64 {
109 self.0
110 }
111
112 pub fn from_hex_string(hex_str: &str) -> Option<Self> {
116 let trimmed = hex_str.trim_start_matches("0x");
117 if trimmed.is_empty() {
118 return None;
119 }
120 let val = u64::from_str_radix(trimmed, 16).ok()?;
121 Some(GroupId(val))
122 }
123
124 pub fn new_random() -> Self {
126 use std::collections::hash_map::DefaultHasher;
127 use std::hash::{Hash, Hasher};
128 use std::time::{SystemTime, UNIX_EPOCH};
129
130 let nanos = SystemTime::now()
131 .duration_since(UNIX_EPOCH)
132 .unwrap_or_default()
133 .as_nanos();
134 let mut hasher = DefaultHasher::new();
135 nanos.hash(&mut hasher);
136 rand::random::<u64>().hash(&mut hasher);
137 GroupId(hasher.finish())
138 }
139
140 pub fn to_hex_string(&self) -> String {
142 format!("{:016x}", self.0)
143 }
144}
145
146#[derive(Debug, Clone)]
147pub struct DownloadOptions {
148 pub split: Option<u16>,
149 pub max_connection_per_server: Option<u16>,
150 pub max_download_limit: Option<u64>,
151 pub max_upload_limit: Option<u64>,
152 pub dir: Option<String>,
153 pub out: Option<String>,
154 pub file_allocation: Option<String>,
157 pub mmap_threshold: Option<u64>,
160 pub secure_falloc: bool,
166 pub seed_time: Option<u64>,
167 pub seed_ratio: Option<f64>,
168 pub checksum: Option<(String, String)>,
169 pub cookie_file: Option<String>,
170 pub cookies: Option<String>,
171 pub bt_force_encrypt: bool,
172 pub bt_require_crypto: bool,
173 pub enable_dht: bool,
174 pub dht_listen_port: Option<u16>,
175 pub dht_entry_point: Option<Vec<String>>,
176 pub enable_public_trackers: bool,
177 pub bt_piece_selection_strategy: String,
178 pub bt_endgame_threshold: u32,
179 pub max_retries: u32,
180 pub retry_wait: u64,
181 pub http_proxy: Option<String>,
182 pub all_proxy: Option<String>,
183 pub https_proxy: Option<String>,
184 pub ftp_proxy: Option<String>,
185 pub no_proxy: Option<String>,
186 pub dht_file_path: Option<String>,
187
188 pub bt_max_upload_slots: Option<u32>,
194
195 pub bt_optimistic_unchoke_interval: Option<u64>,
198
199 pub bt_snubbed_timeout: Option<u64>,
202
203 pub bt_prioritize_piece: String,
209
210 pub enable_utp: bool,
217
218 pub utp_listen_port: Option<u16>,
221
222 pub header: Vec<String>,
228 pub user_agent: Option<String>,
231 pub referer: Option<String>,
234}
235
236impl Default for DownloadOptions {
240 fn default() -> Self {
241 Self {
242 split: None,
243 max_connection_per_server: None,
244 max_download_limit: None,
245 max_upload_limit: None,
246 dir: None,
247 out: None,
248 file_allocation: None,
249 mmap_threshold: None,
250 secure_falloc: false,
251 seed_time: None,
252 seed_ratio: None,
253 checksum: None,
254 cookie_file: None,
255 cookies: None,
256 bt_force_encrypt: false,
257 bt_require_crypto: false,
258 enable_dht: true,
259 dht_listen_port: None,
260 dht_entry_point: None,
261 enable_public_trackers: true,
262 bt_piece_selection_strategy: String::new(),
263 bt_endgame_threshold: 0,
264 max_retries: 0,
265 retry_wait: 0,
266 http_proxy: None,
267 all_proxy: None,
268 https_proxy: None,
269 ftp_proxy: None,
270 no_proxy: None,
271 dht_file_path: None,
272 bt_max_upload_slots: None,
273 bt_optimistic_unchoke_interval: None,
274 bt_snubbed_timeout: None,
275 bt_prioritize_piece: String::new(),
276 enable_utp: false,
277 utp_listen_port: None,
278 header: Vec::new(),
279 user_agent: None,
280 referer: None,
281 }
282 }
283}
284
285impl DownloadOptions {
286 pub fn parsed_headers(&self) -> Vec<(String, String)> {
292 let mut result: Vec<(String, String)> = Vec::new();
293 for raw in &self.header {
294 if let Some((name, value)) = raw.split_once(':') {
295 let name = name.trim().to_string();
296 let value = value.trim().to_string();
297 if !name.is_empty() {
298 result.push((name, value));
299 }
300 }
301 }
302 if let Some(ref ua) = self.user_agent
304 && !has_header(&result, "User-Agent")
305 {
306 result.push(("User-Agent".to_string(), ua.clone()));
307 }
308 if let Some(ref ref_) = self.referer
309 && !has_header(&result, "Referer")
310 {
311 result.push(("Referer".to_string(), ref_.clone()));
312 }
313 result
314 }
315}
316
317fn has_header(headers: &[(String, String)], name: &str) -> bool {
320 headers.iter().any(|(n, _)| n.eq_ignore_ascii_case(name))
321}
322
323pub const RUNTIME_CHANGEABLE_OPTIONS: &[&str] = &[
327 "split",
328 "max-connection-per-server",
329 "max-download-limit",
330 "max-upload-limit",
331 "max-retries",
332 "retry-wait",
333 "header",
334 "user-agent",
335 "referer",
336 "bt-max-upload-slots",
337 "bt-snubbed-timeout",
338 "bt-optimistic-unchoke-interval",
339 "bt-endgame-threshold",
340 "seed-time",
341 "seed-ratio",
342];
343
344pub struct RequestGroup {
345 gid: GroupId,
346 uris: Vec<String>,
347 options: DownloadOptions,
348 status: Arc<RwLock<DownloadStatus>>,
349 segments: Arc<RwLock<Vec<Segment>>>,
350 total_length: u64,
351 completed_length: Arc<RwLock<u64>>,
352 download_speed: Arc<RwLock<u64>>,
353 upload_speed: Arc<RwLock<u64>>,
354 start_time: Arc<RwLock<Option<std::time::Instant>>>,
355 end_time: Arc<RwLock<Option<std::time::Instant>>>,
356 pub completed_length_atomic: AtomicU64,
358 pub total_length_atomic: AtomicU64,
359 pub uploaded_length: AtomicU64,
360 pub download_speed_cached: AtomicU64,
361 pub bt_bitfield: RwLock<Option<Vec<u8>>>,
362
363 pub bt_num_pieces: AtomicU32,
366 pub bt_piece_length: AtomicU32,
368 pub bt_info_hash_hex: std::sync::RwLock<Option<String>>,
371
372 pub rate_limiter: RwLock<Option<RateLimiter>>,
378}
379
380impl RequestGroup {
381 pub fn new(gid: GroupId, uris: Vec<String>, options: DownloadOptions) -> Self {
382 info!("Creating request group #{}", gid.value());
383
384 RequestGroup {
385 gid,
386 uris,
387 options,
388 status: Arc::new(RwLock::new(DownloadStatus::Waiting)),
389 segments: Arc::new(RwLock::new(Vec::new())),
390 total_length: 0,
391 completed_length: Arc::new(RwLock::new(0)),
392 download_speed: Arc::new(RwLock::new(0)),
393 upload_speed: Arc::new(RwLock::new(0)),
394 start_time: Arc::new(RwLock::new(None)),
395 end_time: Arc::new(RwLock::new(None)),
396 completed_length_atomic: AtomicU64::new(0),
398 total_length_atomic: AtomicU64::new(0),
399 uploaded_length: AtomicU64::new(0),
400 download_speed_cached: AtomicU64::new(0),
401 bt_bitfield: RwLock::new(None),
402
403 bt_num_pieces: AtomicU32::new(0),
405 bt_piece_length: AtomicU32::new(0),
406 bt_info_hash_hex: std::sync::RwLock::new(None),
407
408 rate_limiter: RwLock::new(None),
411 }
412 }
413
414 pub async fn start(&mut self) -> Result<()> {
415 let mut status = self.status.write().await;
416 let mut start_time = self.start_time.write().await;
417
418 *status = DownloadStatus::Active;
419 *start_time = Some(std::time::Instant::now());
420
421 info!("Starting download task #{}", self.gid.value());
422 Ok(())
423 }
424
425 pub async fn pause(&mut self) -> Result<()> {
426 let mut status = self.status.write().await;
427
428 if matches!(*status, DownloadStatus::Active) {
429 *status = DownloadStatus::Paused;
430 info!("Pausing download task #{}", self.gid.value());
431 }
432
433 Ok(())
434 }
435
436 pub async fn remove(&mut self) -> Result<()> {
437 let mut status = self.status.write().await;
438 let mut end_time = self.end_time.write().await;
439
440 *status = DownloadStatus::Removed;
441 *end_time = Some(std::time::Instant::now());
442
443 info!("Removing download task #{}", self.gid.value());
444 Ok(())
445 }
446
447 pub async fn complete(&mut self) -> Result<()> {
448 let mut status = self.status.write().await;
449 let mut end_time = self.end_time.write().await;
450 let mut completed_length = self.completed_length.write().await;
451
452 *status = DownloadStatus::Complete;
453 *end_time = Some(std::time::Instant::now());
454 *completed_length = self.total_length;
455
456 info!("Completing download task #{}", self.gid.value());
457 Ok(())
458 }
459
460 pub async fn error(&mut self, err: impl Into<String>) -> Result<()> {
461 let mut status = self.status.write().await;
462 let mut end_time = self.end_time.write().await;
463
464 *status = DownloadStatus::Error(err.into());
465 *end_time = Some(std::time::Instant::now());
466
467 debug!("Download task #{} encountered error", self.gid.value());
468 Ok(())
469 }
470
471 pub async fn status(&self) -> DownloadStatus {
472 self.status.read().await.clone()
473 }
474
475 pub fn gid(&self) -> GroupId {
476 self.gid
477 }
478
479 pub fn uris(&self) -> &[String] {
480 &self.uris
481 }
482
483 pub fn options(&self) -> &DownloadOptions {
484 &self.options
485 }
486
487 pub async fn set_rate_limiter(&self, limiter: RateLimiter) {
495 *self.rate_limiter.write().await = Some(limiter);
496 }
497
498 pub async fn update_option(&mut self, key: &str, value: serde_json::Value) -> bool {
510 match key {
511 "split" => {
512 if let Some(v) = value.as_u64() {
513 self.options.split = Some(v as u16);
514 tracing::warn!(
515 new_split = v,
516 "split changed but will take effect on download restart/retry, \
517 not mid-download (current segments unchanged)"
518 );
519 }
520 true
521 }
522 "max-download-limit" => {
523 let rate = value.as_u64();
524 self.options.max_download_limit = rate;
525 if let Some(ref limiter) = *self.rate_limiter.read().await {
526 limiter.set_download_rate(rate);
527 }
528 true
529 }
530 "max-upload-limit" => {
531 let rate = value.as_u64();
532 self.options.max_upload_limit = rate;
533 if let Some(ref limiter) = *self.rate_limiter.read().await {
534 limiter.set_upload_rate(rate);
535 }
536 true
537 }
538 "max-retries" => {
539 if let Some(v) = value.as_u64() {
540 self.options.max_retries = v as u32;
541 }
542 true
543 }
544 "retry-wait" => {
545 if let Some(v) = value.as_u64() {
546 self.options.retry_wait = v;
547 }
548 true
549 }
550 "header" => {
551 match &value {
554 serde_json::Value::Array(arr) => {
555 self.options.header = arr
556 .iter()
557 .filter_map(|v| v.as_str().map(|s| s.to_string()))
558 .collect();
559 }
560 serde_json::Value::String(s) => {
561 self.options.header = s
562 .split('\n')
563 .map(|l| l.trim().to_string())
564 .filter(|l| !l.is_empty())
565 .collect();
566 }
567 _ => {}
568 }
569 true
570 }
571 "user-agent" => {
572 self.options.user_agent = value.as_str().map(|s| s.to_string());
573 true
574 }
575 "referer" => {
576 self.options.referer = value.as_str().map(|s| s.to_string());
577 true
578 }
579 "max-connection-per-server" => {
580 if let Some(v) = value.as_u64() {
581 self.options.max_connection_per_server = Some(v as u16);
582 }
583 true
584 }
585 "bt-max-upload-slots" => {
586 if let Some(v) = value.as_u64() {
587 self.options.bt_max_upload_slots = Some(v as u32);
588 }
589 true
590 }
591 "bt-snubbed-timeout" => {
592 if let Some(v) = value.as_u64() {
593 self.options.bt_snubbed_timeout = Some(v);
594 }
595 true
596 }
597 "bt-optimistic-unchoke-interval" => {
598 if let Some(v) = value.as_u64() {
599 self.options.bt_optimistic_unchoke_interval = Some(v);
600 }
601 true
602 }
603 "bt-endgame-threshold" => {
604 if let Some(v) = value.as_u64() {
605 self.options.bt_endgame_threshold = v as u32;
606 }
607 true
608 }
609 "seed-time" => {
610 if let Some(v) = value.as_u64() {
611 self.options.seed_time = Some(v);
612 }
613 true
614 }
615 "seed-ratio" => {
616 if let Some(v) = value.as_f64() {
617 self.options.seed_ratio = Some(v);
618 }
619 true
620 }
621 _ => false,
622 }
623 }
624
625 pub fn total_length(&self) -> u64 {
626 self.total_length
627 }
628
629 pub async fn set_total_length(&mut self, length: u64) {
630 self.total_length = length;
631 debug!("Setting total length: {} bytes", length);
632 }
633
634 pub async fn completed_length(&self) -> u64 {
635 *self.completed_length.read().await
636 }
637
638 pub async fn update_completed_length(&self, length: u64) {
639 let mut completed_length = self.completed_length.write().await;
640 *completed_length = length;
641 }
642
643 pub async fn update_progress(&self, completed_length: u64) {
644 let mut cl = self.completed_length.write().await;
645 *cl = completed_length;
646 }
647
648 pub async fn progress(&self) -> f64 {
649 let total = self.total_length;
650 let completed = *self.completed_length.read().await;
651
652 if total == 0 {
653 0.0
654 } else {
655 (completed as f64 / total as f64) * 100.0
656 }
657 }
658
659 pub async fn download_speed(&self) -> u64 {
660 *self.download_speed.read().await
661 }
662
663 pub async fn upload_speed(&self) -> u64 {
664 *self.upload_speed.read().await
665 }
666
667 pub async fn update_speed(&self, dl_speed: u64, ul_speed: u64) {
668 let mut ds = self.download_speed.write().await;
669 let mut us = self.upload_speed.write().await;
670 *ds = dl_speed;
671 *us = ul_speed;
672 }
673
674 pub async fn add_segment(&mut self, segment: Segment) {
675 let mut segments = self.segments.write().await;
676 segments.push(segment);
677 debug!("Adding segment, current segments: {}", segments.len());
678 }
679
680 pub async fn segments(&self) -> Vec<Segment> {
681 self.segments.read().await.clone()
682 }
683
684 pub async fn elapsed_time(&self) -> Option<std::time::Duration> {
685 let start = *self.start_time.read().await;
686 start.map(|t| t.elapsed())
687 }
688
689 pub async fn eta(&self) -> Option<std::time::Duration> {
690 let speed = *self.download_speed.read().await;
691 let remaining = self
692 .total_length
693 .saturating_sub(*self.completed_length.read().await);
694
695 if speed == 0 || remaining == 0 {
696 None
697 } else {
698 Some(std::time::Duration::from_secs(remaining / speed))
699 }
700 }
701
702 pub fn set_completed_length(&self, val: u64) {
707 self.completed_length_atomic.store(val, Ordering::Relaxed);
708 }
709
710 pub fn get_completed_length(&self) -> u64 {
712 self.completed_length_atomic.load(Ordering::Relaxed)
713 }
714
715 pub fn set_total_length_atomic(&self, val: u64) {
717 self.total_length_atomic.store(val, Ordering::Relaxed);
718 }
719
720 pub fn get_total_length_atomic(&self) -> u64 {
722 self.total_length_atomic.load(Ordering::Relaxed)
723 }
724
725 pub fn set_uploaded_length(&self, val: u64) {
727 self.uploaded_length.store(val, Ordering::Relaxed);
728 }
729
730 pub fn get_uploaded_length(&self) -> u64 {
732 self.uploaded_length.load(Ordering::Relaxed)
733 }
734
735 pub fn set_download_speed_cached(&self, val: u64) {
737 self.download_speed_cached.store(val, Ordering::Relaxed);
738 }
739
740 pub fn get_download_speed_cached(&self) -> u64 {
742 self.download_speed_cached.load(Ordering::Relaxed)
743 }
744
745 pub async fn set_bt_bitfield(&self, bf: Option<Vec<u8>>) {
747 *self.bt_bitfield.write().await = bf;
748 }
749
750 pub async fn get_bt_bitfield(&self) -> Option<Vec<u8>> {
752 self.bt_bitfield.read().await.clone()
753 }
754
755 pub fn set_resume_offset(&self, offset: u64) {
757 self.completed_length_atomic
760 .store(offset, Ordering::Relaxed);
761 }
762
763 pub fn set_bt_metadata(&self, num_pieces: u32, piece_length: u32, info_hash_hex: String) {
768 self.bt_num_pieces.store(num_pieces, Ordering::Relaxed);
769 self.bt_piece_length.store(piece_length, Ordering::Relaxed);
770 *self.bt_info_hash_hex.write().unwrap() = Some(info_hash_hex);
772 }
773
774 pub fn get_bt_num_pieces(&self) -> u32 {
776 self.bt_num_pieces.load(Ordering::Relaxed)
777 }
778
779 pub fn get_bt_piece_length(&self) -> u32 {
781 self.bt_piece_length.load(Ordering::Relaxed)
782 }
783
784 pub async fn get_bt_info_hash_hex_async(&self) -> Option<String> {
786 self.bt_info_hash_hex.read().unwrap().clone()
788 }
789
790 pub fn get_bt_info_hash_hex(&self) -> Option<String> {
792 self.bt_info_hash_hex.read().unwrap().clone()
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn test_request_group_progress_fields_default() {
802 let group = RequestGroup::new(
804 GroupId::new(1),
805 vec!["http://example.com/file.zip".to_string()],
806 DownloadOptions::default(),
807 );
808
809 assert_eq!(
811 group.get_completed_length(),
812 0,
813 "completed_length_atomic should default to 0"
814 );
815 assert_eq!(
816 group.get_total_length_atomic(),
817 0,
818 "total_length_atomic should default to 0"
819 );
820 assert_eq!(
821 group.get_uploaded_length(),
822 0,
823 "uploaded_length should default to 0"
824 );
825 assert_eq!(
826 group.get_download_speed_cached(),
827 0,
828 "download_speed_cached should default to 0"
829 );
830 }
831
832 #[test]
833 fn test_set_get_completed_length() {
834 let group = RequestGroup::new(
835 GroupId::new(2),
836 vec!["http://test.com/file.bin".to_string()],
837 DownloadOptions::default(),
838 );
839
840 group.set_completed_length(1024);
842 assert_eq!(
843 group.get_completed_length(),
844 1024,
845 "Should return 1024 after setting"
846 );
847
848 group.set_completed_length(2048);
850 assert_eq!(
851 group.get_completed_length(),
852 2048,
853 "Should return 2048 after update"
854 );
855
856 group.set_completed_length(u64::MAX);
858 assert_eq!(
859 group.get_completed_length(),
860 u64::MAX,
861 "Should handle u64::MAX"
862 );
863
864 group.set_completed_length(0);
866 assert_eq!(group.get_completed_length(), 0, "Should handle 0");
867 }
868
869 #[test]
870 fn test_set_get_total_length() {
871 let group = RequestGroup::new(
872 GroupId::new(3),
873 vec!["http://example.com/large.iso".to_string()],
874 DownloadOptions::default(),
875 );
876
877 group.set_total_length_atomic(1048576); assert_eq!(
880 group.get_total_length_atomic(),
881 1048576,
882 "Should return 1MB after setting"
883 );
884
885 group.set_total_length_atomic(1073741824); assert_eq!(
888 group.get_total_length_atomic(),
889 1073741824,
890 "Should return 1GB after update"
891 );
892 }
893
894 #[tokio::test]
895 async fn test_set_get_bt_bitfield() {
896 let group = RequestGroup::new(
897 GroupId::new(4),
898 vec!["magnet:?xt=urn:btih:abc123".to_string()],
899 DownloadOptions::default(),
900 );
901
902 let bf = group.get_bt_bitfield().await;
904 assert!(bf.is_none(), "bt_bitfield should default to None");
905
906 let test_bitfield = vec![0xFF, 0xF0, 0x0F];
908 group.set_bt_bitfield(Some(test_bitfield.clone())).await;
909 let retrieved = group.get_bt_bitfield().await;
910 assert!(
911 retrieved.is_some(),
912 "bt_bitfield should be Some after setting"
913 );
914 assert_eq!(
915 retrieved.unwrap(),
916 test_bitfield,
917 "bitfield should match what was set"
918 );
919
920 group.set_bt_bitfield(None).await;
922 let bf_none = group.get_bt_bitfield().await;
923 assert!(
924 bf_none.is_none(),
925 "bt_bitfield should be None after clearing"
926 );
927
928 group.set_bt_bitfield(Some(vec![])).await;
930 let empty_bf = group.get_bt_bitfield().await;
931 assert!(empty_bf.is_some(), "empty bitfield should still be Some");
932 assert!(empty_bf.unwrap().is_empty(), "bitfield should be empty vec");
933 }
934
935 #[tokio::test]
936 async fn test_concurrent_access() {
937 use std::sync::Arc;
938
939 let group = Arc::new(RequestGroup::new(
940 GroupId::new(5),
941 vec!["http://load.test/file.dat".to_string()],
942 DownloadOptions::default(),
943 ));
944
945 let mut handles = Vec::new();
947
948 for i in 0..10 {
949 let g = Arc::clone(&group);
950 handles.push(tokio::spawn(async move {
951 g.set_completed_length(i * 100);
953 g.set_total_length_atomic(10000);
954 g.set_uploaded_length(i * 10);
955 g.set_download_speed_cached(i * 1000);
956
957 let _cl = g.get_completed_length();
959 let _tl = g.get_total_length_atomic();
960 let _ul = g.get_uploaded_length();
961 let _ds = g.get_download_speed_cached();
962
963 if i % 3 == 0 {
965 let bf = vec![i as u8; 8];
966 g.set_bt_bitfield(Some(bf)).await;
967 let _retrieved = g.get_bt_bitfield().await;
968 }
969
970 tokio::time::sleep(std::time::Duration::from_millis(1)).await;
972 }));
973 }
974
975 for handle in handles {
977 handle.await.expect("Task should complete without panic");
978 }
979
980 let final_cl = group.get_completed_length();
982 let final_tl = group.get_total_length_atomic();
983 let final_ul = group.get_uploaded_length();
984 let final_ds = group.get_download_speed_cached();
985
986 assert!(final_cl <= 900, "completed_length should be <= 900");
988 assert_eq!(final_tl, 10000, "total_length should be 10000");
989 assert!(final_ul <= 90, "uploaded_length should be <= 90");
990 assert!(final_ds <= 9000, "download_speed should be <= 9000");
991 }
992
993 #[test]
994 fn test_set_get_uploaded_length() {
995 let group = RequestGroup::new(
996 GroupId::new(6),
997 vec!["http://seed.test/file.torrent".to_string()],
998 DownloadOptions::default(),
999 );
1000
1001 assert_eq!(group.get_uploaded_length(), 0);
1003
1004 group.set_uploaded_length(512);
1006 assert_eq!(group.get_uploaded_length(), 512);
1007
1008 group.set_uploaded_length(u64::MAX / 2);
1010 assert_eq!(group.get_uploaded_length(), u64::MAX / 2);
1011 }
1012
1013 #[test]
1014 fn test_set_get_download_speed_cached() {
1015 let group = RequestGroup::new(
1016 GroupId::new(7),
1017 vec!["http://speed.test/large.file".to_string()],
1018 DownloadOptions::default(),
1019 );
1020
1021 assert_eq!(group.get_download_speed_cached(), 0);
1023
1024 group.set_download_speed_cached(5242880);
1026 assert_eq!(group.get_download_speed_cached(), 5242880);
1027
1028 group.set_download_speed_cached(10485760); assert_eq!(group.get_download_speed_cached(), 10485760);
1031 }
1032
1033 #[test]
1034 fn test_download_options_choking_config_defaults() {
1035 let opts = DownloadOptions::default();
1037
1038 assert!(
1039 opts.bt_max_upload_slots.is_none(),
1040 "bt_max_upload_slots should default to None"
1041 );
1042 assert!(
1043 opts.bt_optimistic_unchoke_interval.is_none(),
1044 "bt_optimistic_unchoke_interval should default to None"
1045 );
1046 assert!(
1047 opts.bt_snubbed_timeout.is_none(),
1048 "bt_snubbed_timeout should default to None"
1049 );
1050 }
1051
1052 #[test]
1053 fn test_download_options_choking_config_custom() {
1054 let opts = DownloadOptions {
1056 bt_max_upload_slots: Some(8),
1057 bt_optimistic_unchoke_interval: Some(15),
1058 bt_snubbed_timeout: Some(45),
1059 ..DownloadOptions::default()
1060 };
1061
1062 assert_eq!(opts.bt_max_upload_slots, Some(8));
1063 assert_eq!(opts.bt_optimistic_unchoke_interval, Some(15));
1064 assert_eq!(opts.bt_snubbed_timeout, Some(45));
1065 }
1066
1067 #[test]
1068 fn test_download_options_choking_config_clone() {
1069 let opts = DownloadOptions {
1071 bt_max_upload_slots: Some(6),
1072 bt_optimistic_unchoke_interval: Some(20),
1073 bt_snubbed_timeout: Some(90),
1074 ..DownloadOptions::default()
1075 };
1076
1077 let cloned = opts.clone();
1078
1079 assert_eq!(cloned.bt_max_upload_slots, Some(6));
1080 assert_eq!(cloned.bt_optimistic_unchoke_interval, Some(20));
1081 assert_eq!(cloned.bt_snubbed_timeout, Some(90));
1082 }
1083
1084 #[test]
1087 fn test_bt_metadata_defaults() {
1088 let group = RequestGroup::new(
1089 GroupId::new(8),
1090 vec!["http://example.com/file.zip".to_string()],
1091 DownloadOptions::default(),
1092 );
1093
1094 assert_eq!(
1096 group.get_bt_num_pieces(),
1097 0,
1098 "bt_num_pieces should default to 0"
1099 );
1100 assert_eq!(
1101 group.get_bt_piece_length(),
1102 0,
1103 "bt_piece_length should default to 0"
1104 );
1105 assert_eq!(
1106 group.get_bt_info_hash_hex(),
1107 None,
1108 "bt_info_hash_hex should default to None"
1109 );
1110 }
1111
1112 #[test]
1113 fn test_set_bt_metadata() {
1114 let group = RequestGroup::new(
1115 GroupId::new(9),
1116 vec!["magnet:?xt=urn:btih:abc123def456".to_string()],
1117 DownloadOptions::default(),
1118 );
1119
1120 group.set_bt_metadata(
1122 100,
1123 262144,
1124 "abc123def456789012345678901234567890abcd".to_string(),
1125 );
1126
1127 assert_eq!(group.get_bt_num_pieces(), 100);
1129 assert_eq!(group.get_bt_piece_length(), 262144); assert_eq!(
1131 group.get_bt_info_hash_hex(),
1132 Some("abc123def456789012345678901234567890abcd".to_string())
1133 );
1134 }
1135
1136 #[test]
1137 fn test_bt_metadata_update() {
1138 let group = RequestGroup::new(
1139 GroupId::new(10),
1140 vec!["bt://test.torrent".to_string()],
1141 DownloadOptions::default(),
1142 );
1143
1144 group.set_bt_metadata(50, 16384, "first_hash".to_string());
1146 assert_eq!(group.get_bt_num_pieces(), 50);
1147
1148 group.set_bt_metadata(200, 524288, "updated_hash".to_string());
1150 assert_eq!(group.get_bt_num_pieces(), 200);
1151 assert_eq!(group.get_bt_piece_length(), 524288);
1152 assert_eq!(
1153 group.get_bt_info_hash_hex(),
1154 Some("updated_hash".to_string())
1155 );
1156 }
1157
1158 #[tokio::test]
1159 async fn test_bt_info_hash_hex_async() {
1160 let group = RequestGroup::new(
1161 GroupId::new(11),
1162 vec!["magnet:?xt=urn:btih:test".to_string()],
1163 DownloadOptions::default(),
1164 );
1165
1166 group.set_bt_metadata(10, 1024, "async_test_hash".to_string());
1168
1169 let hash = group.get_bt_info_hash_hex_async().await;
1171 assert_eq!(hash, Some("async_test_hash".to_string()));
1172 }
1173
1174 #[tokio::test]
1175 async fn test_update_option_new_runtime_changeable() {
1176 let gid = GroupId::new(1);
1177 let uris = vec!["http://example.com/file".to_string()];
1178 let mut group = RequestGroup::new(gid, uris, DownloadOptions::default());
1179
1180 assert!(
1182 group
1183 .update_option("max-connection-per-server", serde_json::json!(4))
1184 .await
1185 );
1186 assert_eq!(group.options().max_connection_per_server, Some(4));
1187
1188 assert!(
1190 group
1191 .update_option("bt-max-upload-slots", serde_json::json!(8))
1192 .await
1193 );
1194 assert_eq!(group.options().bt_max_upload_slots, Some(8));
1195
1196 assert!(
1198 group
1199 .update_option("bt-snubbed-timeout", serde_json::json!(120))
1200 .await
1201 );
1202 assert_eq!(group.options().bt_snubbed_timeout, Some(120));
1203
1204 assert!(
1206 group
1207 .update_option("bt-optimistic-unchoke-interval", serde_json::json!(45))
1208 .await
1209 );
1210 assert_eq!(group.options().bt_optimistic_unchoke_interval, Some(45));
1211
1212 assert!(
1214 group
1215 .update_option("bt-endgame-threshold", serde_json::json!(50))
1216 .await
1217 );
1218 assert_eq!(group.options().bt_endgame_threshold, 50);
1219
1220 assert!(
1222 group
1223 .update_option("seed-time", serde_json::json!(3600))
1224 .await
1225 );
1226 assert_eq!(group.options().seed_time, Some(3600));
1227
1228 assert!(
1230 group
1231 .update_option("seed-ratio", serde_json::json!(2.0))
1232 .await
1233 );
1234 assert_eq!(group.options().seed_ratio, Some(2.0));
1235
1236 assert!(
1238 !group
1239 .update_option("unknown-option", serde_json::json!(1))
1240 .await
1241 );
1242 }
1243}