From 601c358d0ff55965a3d172d6370c26aa623c5df7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 5 Mar 2024 16:09:30 +0000 Subject: [PATCH] feat: [#294] add canonical info-hash group top torrent details API response ```json { "data": { "torrent_id": 2, "uploader": "admin", "info_hash": "0c90fbf036e28370c1ec773401bc7620146b1d48", "title": "Test 01", "description": "Test 01", "category": { "id": 5, "category_id": 5, "name": "software", "num_torrents": 1 }, "upload_date": "2024-03-05 16:05:00", "file_size": 602515, "seeders": 0, "leechers": 0, "files": [ { "path": [ "mandelbrot_set_01" ], "length": 602515, "md5sum": null } ], "trackers": [ "udp://localhost:6969" ], "magnet_link": "magnet:?xt=urn:btih:0c90fbf036e28370c1ec773401bc7620146b1d48&dn=Test%2001&tr=udp%3A%2F%2Flocalhost%3A6969", "tags": [], "name": "mandelbrot_set_01", "comment": "Mandelbrot Set 01", "creation_date": 1687937540, "created_by": "Transmission/3.00 (bb6b5a062e)", "encoding": "UTF-8", "canonical_info_hash_group": [ "d5eaff5bc75ed274da7c5294de3f6641dc0a90ce", "e126f473a9dee89217d7ae5982f9b21490ed2c3f" ] } } ``` Notice the new field: `canonical_info_hash_group` at the end of the JSON. --- src/models/response.rs | 13 +- src/services/torrent.rs | 190 ++++++++++-------- tests/common/contexts/torrent/responses.rs | 1 + .../web/api/v1/contexts/torrent/contract.rs | 3 +- 4 files changed, 123 insertions(+), 84 deletions(-) diff --git a/src/models/response.rs b/src/models/response.rs index dedc067f..020eb9be 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -6,6 +6,7 @@ use crate::databases::database::Category as DatabaseCategory; use crate::models::torrent::TorrentListing; use crate::models::torrent_file::TorrentFile; use crate::models::torrent_tag::TorrentTag; +use crate::services::torrent::CanonicalInfoHashGroup; pub enum OkResponses { TokenResponse(TokenResponse), @@ -67,11 +68,16 @@ pub struct TorrentResponse { pub creation_date: Option, pub created_by: Option, pub encoding: Option, + pub canonical_info_hash_group: Vec, } impl TorrentResponse { #[must_use] - pub fn from_listing(torrent_listing: TorrentListing, category: Option) -> TorrentResponse { + pub fn from_listing( + torrent_listing: TorrentListing, + category: Option, + canonical_info_hash_group: &CanonicalInfoHashGroup, + ) -> TorrentResponse { TorrentResponse { torrent_id: torrent_listing.torrent_id, uploader: torrent_listing.uploader, @@ -92,6 +98,11 @@ impl TorrentResponse { creation_date: torrent_listing.creation_date, created_by: torrent_listing.created_by, encoding: torrent_listing.encoding, + canonical_info_hash_group: canonical_info_hash_group + .original_info_hashes + .iter() + .map(super::info_hash::InfoHash::to_hex_string) + .collect(), } } diff --git a/src/services/torrent.rs b/src/services/torrent.rs index 20f4a2a3..bc197881 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -328,82 +328,9 @@ impl Index { ) -> Result { let torrent_listing = self.torrent_listing_generator.one_torrent_by_info_hash(info_hash).await?; - let torrent_id = torrent_listing.torrent_id; - - let category = match torrent_listing.category_id { - Some(category_id) => Some(self.category_repository.get_by_id(&category_id).await?), - None => None, - }; - - let mut torrent_response = TorrentResponse::from_listing(torrent_listing, category); - - // Add files - - torrent_response.files = self.torrent_file_repository.get_by_torrent_id(&torrent_id).await?; - - if torrent_response.files.len() == 1 { - let torrent_info = self.torrent_info_repository.get_by_info_hash(info_hash).await?; - - torrent_response - .files - .iter_mut() - .for_each(|v| v.path = vec![torrent_info.name.to_string()]); - } - - // Add trackers - - // code-review: duplicate logic. We have to check the same in the - // download torrent file endpoint. Here he have only one list of tracker - // like the `announce_list` in the torrent file. - - torrent_response.trackers = self.torrent_announce_url_repository.get_by_torrent_id(&torrent_id).await?; - - let tracker_url = self.get_tracker_url().await; - let tracker_mode = self.get_tracker_mode().await; - - if tracker_mode.is_open() { - torrent_response.include_url_as_main_tracker(&tracker_url); - } else { - // Add main tracker URL - match opt_user_id { - Some(user_id) => { - let personal_announce_url = self.tracker_service.get_personal_announce_url(user_id).await?; - - torrent_response.include_url_as_main_tracker(&personal_announce_url); - } - None => { - torrent_response.include_url_as_main_tracker(&tracker_url); - } - } - } - - // Add magnet link - - // todo: extract a struct or function to build the magnet links - let mut magnet = format!( - "magnet:?xt=urn:btih:{}&dn={}", - torrent_response.info_hash, - urlencoding::encode(&torrent_response.title) - ); - - // Add trackers from torrent file to magnet link - for tracker in &torrent_response.trackers { - magnet.push_str(&format!("&tr={}", urlencoding::encode(tracker))); - } - - torrent_response.magnet_link = magnet; - - // Get realtime seeders and leechers - if let Ok(torrent_info) = self - .tracker_statistics_importer - .import_torrent_statistics(torrent_response.torrent_id, &torrent_response.info_hash) - .await - { - torrent_response.seeders = torrent_info.seeders; - torrent_response.leechers = torrent_info.leechers; - } - - torrent_response.tags = self.torrent_tag_repository.get_tags_for_torrent(&torrent_id).await?; + let torrent_response = self + .build_full_torrent_response(torrent_listing, info_hash, opt_user_id) + .await?; Ok(torrent_response) } @@ -497,12 +424,7 @@ impl Index { .one_torrent_by_torrent_id(&torrent_listing.torrent_id) .await?; - let category = match torrent_listing.category_id { - Some(category_id) => Some(self.category_repository.get_by_id(&category_id).await?), - None => None, - }; - - let torrent_response = TorrentResponse::from_listing(torrent_listing, category); + let torrent_response = self.build_short_torrent_response(torrent_listing, info_hash).await?; Ok(torrent_response) } @@ -516,6 +438,109 @@ impl Index { let settings = self.configuration.settings.read().await; settings.tracker.mode.clone() } + + async fn build_short_torrent_response( + &self, + torrent_listing: TorrentListing, + info_hash: &InfoHash, + ) -> Result { + let category = match torrent_listing.category_id { + Some(category_id) => Some(self.category_repository.get_by_id(&category_id).await?), + None => None, + }; + + let canonical_info_hash_group = self + .torrent_info_hash_repository + .get_canonical_info_hash_group(info_hash) + .await?; + + Ok(TorrentResponse::from_listing( + torrent_listing, + category, + &canonical_info_hash_group, + )) + } + + async fn build_full_torrent_response( + &self, + torrent_listing: TorrentListing, + info_hash: &InfoHash, + opt_user_id: Option, + ) -> Result { + let torrent_id: i64 = torrent_listing.torrent_id; + + let mut torrent_response = self.build_short_torrent_response(torrent_listing, info_hash).await?; + + // Add files + + torrent_response.files = self.torrent_file_repository.get_by_torrent_id(&torrent_id).await?; + + if torrent_response.files.len() == 1 { + let torrent_info = self.torrent_info_repository.get_by_info_hash(info_hash).await?; + + torrent_response + .files + .iter_mut() + .for_each(|v| v.path = vec![torrent_info.name.to_string()]); + } + + // Add trackers + + // code-review: duplicate logic. We have to check the same in the + // download torrent file endpoint. Here he have only one list of tracker + // like the `announce_list` in the torrent file. + + torrent_response.trackers = self.torrent_announce_url_repository.get_by_torrent_id(&torrent_id).await?; + + let tracker_url = self.get_tracker_url().await; + let tracker_mode = self.get_tracker_mode().await; + + if tracker_mode.is_open() { + torrent_response.include_url_as_main_tracker(&tracker_url); + } else { + // Add main tracker URL + match opt_user_id { + Some(user_id) => { + let personal_announce_url = self.tracker_service.get_personal_announce_url(user_id).await?; + + torrent_response.include_url_as_main_tracker(&personal_announce_url); + } + None => { + torrent_response.include_url_as_main_tracker(&tracker_url); + } + } + } + + // Add magnet link + + // todo: extract a struct or function to build the magnet links + let mut magnet = format!( + "magnet:?xt=urn:btih:{}&dn={}", + torrent_response.info_hash, + urlencoding::encode(&torrent_response.title) + ); + + // Add trackers from torrent file to magnet link + for tracker in &torrent_response.trackers { + magnet.push_str(&format!("&tr={}", urlencoding::encode(tracker))); + } + + torrent_response.magnet_link = magnet; + + // Get realtime seeders and leechers + if let Ok(torrent_info) = self + .tracker_statistics_importer + .import_torrent_statistics(torrent_response.torrent_id, &torrent_response.info_hash) + .await + { + torrent_response.seeders = torrent_info.seeders; + torrent_response.leechers = torrent_info.leechers; + } + + torrent_response.tags = self.torrent_tag_repository.get_tags_for_torrent(&torrent_id).await?; + + Ok(torrent_response) + } } pub struct DbTorrentRepository { @@ -579,6 +604,7 @@ pub struct DbTorrentInfoHash { /// This function returns the original infohashes of a canonical infohash. /// /// The relationship is 1 canonical infohash -> N original infohashes. +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct CanonicalInfoHashGroup { pub canonical_info_hash: InfoHash, /// The list of original infohashes associated to the canonical one. diff --git a/tests/common/contexts/torrent/responses.rs b/tests/common/contexts/torrent/responses.rs index b1ef0882..55857a5b 100644 --- a/tests/common/contexts/torrent/responses.rs +++ b/tests/common/contexts/torrent/responses.rs @@ -72,6 +72,7 @@ pub struct TorrentDetails { pub creation_date: Option, pub created_by: Option, pub encoding: Option, + pub canonical_info_hash_group: Vec, } #[derive(Deserialize, PartialEq, Debug)] diff --git a/tests/e2e/web/api/v1/contexts/torrent/contract.rs b/tests/e2e/web/api/v1/contexts/torrent/contract.rs index 6c057d67..1d207b56 100644 --- a/tests/e2e/web/api/v1/contexts/torrent/contract.rs +++ b/tests/e2e/web/api/v1/contexts/torrent/contract.rs @@ -197,7 +197,7 @@ mod for_guests { md5sum: None, }], // code-review: why is this duplicated? It seems that is adding the - // same tracker twice because first ti adds all trackers and then + // same tracker twice because first it adds all trackers and then // it adds the tracker with the personal announce url, if the user // is logged in. If the user is not logged in, it adds the default // tracker again, and it ends up with two trackers. @@ -215,6 +215,7 @@ mod for_guests { creation_date: test_torrent.file_info.creation_date, created_by: test_torrent.file_info.created_by.clone(), encoding: test_torrent.file_info.encoding.clone(), + canonical_info_hash_group: vec![test_torrent.file_info.info_hash.to_lowercase()], }; assert_expected_torrent_details(&torrent_details_response.data, &expected_torrent);