Compare commits
No commits in common. "a3609696c7f930c754d6d6e99aea2aeace061a6e" and "ecb56013a5f11138d3cf52a6f3b09787f5dc9bcd" have entirely different histories.
a3609696c7
...
ecb56013a5
@ -23,7 +23,8 @@ where
|
|||||||
&self,
|
&self,
|
||||||
selector: Self::Selector,
|
selector: Self::Selector,
|
||||||
) -> Result<Self::IdSelector, DownloaderError> {
|
) -> Result<Self::IdSelector, DownloaderError> {
|
||||||
let hashes = <Self as TorrentDownloaderTrait>::query_torrent_hashes(self, selector).await?;
|
let hashes =
|
||||||
|
<Self as TorrentDownloaderTrait>::query_torrent_hashes(&self, selector).await?;
|
||||||
self.pause_torrents(hashes).await
|
self.pause_torrents(hashes).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,14 +32,16 @@ where
|
|||||||
&self,
|
&self,
|
||||||
selector: Self::Selector,
|
selector: Self::Selector,
|
||||||
) -> Result<Self::IdSelector, DownloaderError> {
|
) -> Result<Self::IdSelector, DownloaderError> {
|
||||||
let hashes = <Self as TorrentDownloaderTrait>::query_torrent_hashes(self, selector).await?;
|
let hashes =
|
||||||
|
<Self as TorrentDownloaderTrait>::query_torrent_hashes(&self, selector).await?;
|
||||||
self.resume_torrents(hashes).await
|
self.resume_torrents(hashes).await
|
||||||
}
|
}
|
||||||
async fn remove_downloads(
|
async fn remove_downloads(
|
||||||
&self,
|
&self,
|
||||||
selector: Self::Selector,
|
selector: Self::Selector,
|
||||||
) -> Result<Self::IdSelector, DownloaderError> {
|
) -> Result<Self::IdSelector, DownloaderError> {
|
||||||
let hashes = <Self as TorrentDownloaderTrait>::query_torrent_hashes(self, selector).await?;
|
let hashes =
|
||||||
|
<Self as TorrentDownloaderTrait>::query_torrent_hashes(&self, selector).await?;
|
||||||
self.remove_torrents(hashes).await
|
self.remove_torrents(hashes).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ impl TorrentFileSource {
|
|||||||
.boxed()
|
.boxed()
|
||||||
.and_then(|s| {
|
.and_then(|s| {
|
||||||
s.path_segments()
|
s.path_segments()
|
||||||
.and_then(|mut p| p.next_back())
|
.and_then(|p| p.last())
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.ok_or_else(|| anyhow::anyhow!("invalid url"))
|
.ok_or_else(|| anyhow::anyhow!("invalid url"))
|
||||||
.to_dyn_boxed()
|
.to_dyn_boxed()
|
||||||
|
@ -2,6 +2,7 @@ use std::{
|
|||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
|
io,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@ -166,7 +167,6 @@ impl TorrentTaskTrait for QBittorrentTask {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|s| s.trim())
|
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.map(Cow::Borrowed)
|
.map(Cow::Borrowed)
|
||||||
}
|
}
|
||||||
@ -209,7 +209,6 @@ pub struct QBittorrentDownloaderCreation {
|
|||||||
pub save_path: String,
|
pub save_path: String,
|
||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
pub downloader_id: i32,
|
pub downloader_id: i32,
|
||||||
pub wait_sync_timeout: Option<Duration>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type QBittorrentHashSelector = DownloadIdSelector<QBittorrentTask>;
|
pub type QBittorrentHashSelector = DownloadIdSelector<QBittorrentTask>;
|
||||||
@ -355,9 +354,7 @@ impl QBittorrentDownloader {
|
|||||||
endpoint_url,
|
endpoint_url,
|
||||||
subscriber_id: creation.subscriber_id,
|
subscriber_id: creation.subscriber_id,
|
||||||
save_path: creation.save_path.into(),
|
save_path: creation.save_path.into(),
|
||||||
wait_sync_timeout: creation
|
wait_sync_timeout: Duration::from_millis(10000),
|
||||||
.wait_sync_timeout
|
|
||||||
.unwrap_or(Duration::from_secs(10)),
|
|
||||||
downloader_id: creation.downloader_id,
|
downloader_id: creation.downloader_id,
|
||||||
sync_watch: watch::channel(Utc::now()).0,
|
sync_watch: watch::channel(Utc::now()).0,
|
||||||
sync_data: Arc::new(RwLock::new(QBittorrentSyncData::default())),
|
sync_data: Arc::new(RwLock::new(QBittorrentSyncData::default())),
|
||||||
@ -433,12 +430,8 @@ impl QBittorrentDownloader {
|
|||||||
category: &str,
|
category: &str,
|
||||||
) -> Result<(), DownloaderError> {
|
) -> Result<(), DownloaderError> {
|
||||||
{
|
{
|
||||||
let category_no_exists = {
|
|
||||||
let sync_data = self.sync_data.read().await;
|
let sync_data = self.sync_data.read().await;
|
||||||
!sync_data.categories.contains_key(category)
|
if !sync_data.categories.contains_key(category) {
|
||||||
};
|
|
||||||
|
|
||||||
if category_no_exists {
|
|
||||||
self.add_category(category).await?;
|
self.add_category(category).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -499,6 +492,49 @@ impl QBittorrentDownloader {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self, replacer))]
|
||||||
|
pub async fn move_torrent_contents<F: FnOnce(String) -> String>(
|
||||||
|
&self,
|
||||||
|
hash: &str,
|
||||||
|
replacer: F,
|
||||||
|
) -> Result<(), DownloaderError> {
|
||||||
|
let old_path = {
|
||||||
|
let sync_data = self.sync_data.read().await;
|
||||||
|
sync_data
|
||||||
|
.torrents
|
||||||
|
.get(hash)
|
||||||
|
.and_then(|t| t.content_path.as_deref())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"no torrent or torrent does not contain content path",
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
let new_path = replacer(old_path.clone());
|
||||||
|
self.client
|
||||||
|
.rename_file(hash, old_path.clone(), new_path.to_string())
|
||||||
|
.await?;
|
||||||
|
self.wait_sync_until(
|
||||||
|
|sync_data| {
|
||||||
|
let torrents = &sync_data.torrents;
|
||||||
|
torrents.get(hash).is_some_and(|t| {
|
||||||
|
t.content_path.as_deref().is_some_and(|p| {
|
||||||
|
path_equals_as_file_url(p, &new_path)
|
||||||
|
.inspect_err(|error| {
|
||||||
|
tracing::warn!(name = "path_equals_as_file_url", error = ?error);
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip(self))]
|
#[instrument(level = "debug", skip(self))]
|
||||||
pub async fn move_torrents(
|
pub async fn move_torrents(
|
||||||
&self,
|
&self,
|
||||||
@ -548,7 +584,6 @@ impl QBittorrentDownloader {
|
|||||||
Ok(torrent.save_path.take())
|
Ok(torrent.save_path.take())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip(self))]
|
|
||||||
async fn sync_data(&self) -> Result<(), DownloaderError> {
|
async fn sync_data(&self) -> Result<(), DownloaderError> {
|
||||||
let rid = { self.sync_data.read().await.rid };
|
let rid = { self.sync_data.read().await.rid };
|
||||||
let sync_data_patch = self.client.sync(Some(rid)).await?;
|
let sync_data_patch = self.client.sync(Some(rid)).await?;
|
||||||
@ -569,23 +604,17 @@ impl QBittorrentDownloader {
|
|||||||
where
|
where
|
||||||
S: Fn(&QBittorrentSyncData) -> bool,
|
S: Fn(&QBittorrentSyncData) -> bool,
|
||||||
{
|
{
|
||||||
{
|
|
||||||
let sync_data = &self.sync_data.read().await;
|
|
||||||
if stop_wait_fn(sync_data) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = timeout.unwrap_or(self.wait_sync_timeout);
|
let timeout = timeout.unwrap_or(self.wait_sync_timeout);
|
||||||
let start_time = Utc::now();
|
let start_time = Utc::now();
|
||||||
|
|
||||||
let mut receiver = self.sync_watch.subscribe();
|
let mut receiver = self.sync_watch.subscribe();
|
||||||
|
|
||||||
while let Ok(()) = receiver.changed().await {
|
while let Ok(()) = receiver.changed().await {
|
||||||
let has_timeout = {
|
let has_timeout = {
|
||||||
let sync_time = *receiver.borrow();
|
let sync_time = receiver.borrow().clone();
|
||||||
let diff_time = sync_time - start_time;
|
sync_time
|
||||||
diff_time.num_milliseconds() > timeout.as_millis() as i64
|
.signed_duration_since(start_time)
|
||||||
|
.num_milliseconds()
|
||||||
|
> timeout.as_millis() as i64
|
||||||
};
|
};
|
||||||
if has_timeout {
|
if has_timeout {
|
||||||
tracing::warn!(name = "wait_until timeout", timeout = ?timeout);
|
tracing::warn!(name = "wait_until timeout", timeout = ?timeout);
|
||||||
@ -596,7 +625,7 @@ impl QBittorrentDownloader {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
let sync_data = &self.sync_data.read().await;
|
let sync_data = &self.sync_data.read().await;
|
||||||
if stop_wait_fn(sync_data) {
|
if stop_wait_fn(&sync_data) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -617,7 +646,7 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
&self,
|
&self,
|
||||||
creation: Self::Creation,
|
creation: Self::Creation,
|
||||||
) -> Result<HashSet<Self::Id>, DownloaderError> {
|
) -> Result<HashSet<Self::Id>, DownloaderError> {
|
||||||
let tags = {
|
let tag = {
|
||||||
let mut tags = vec![TORRENT_TAG_NAME.to_string()];
|
let mut tags = vec![TORRENT_TAG_NAME.to_string()];
|
||||||
tags.extend(creation.tags);
|
tags.extend(creation.tags);
|
||||||
Some(tags.into_iter().filter(|s| !s.is_empty()).join(","))
|
Some(tags.into_iter().filter(|s| !s.is_empty()).join(","))
|
||||||
@ -626,7 +655,7 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
let save_path = Some(creation.save_path.into_string());
|
let save_path = Some(creation.save_path.into_string());
|
||||||
|
|
||||||
let sources = creation.sources;
|
let sources = creation.sources;
|
||||||
let hashes = HashSet::from_iter(sources.iter().map(|s| s.hash_info().to_string()));
|
let ids = HashSet::from_iter(sources.iter().map(|s| s.hash_info().to_string()));
|
||||||
let (urls_source, files_source) = {
|
let (urls_source, files_source) = {
|
||||||
let mut urls = vec![];
|
let mut urls = vec![];
|
||||||
let mut files = vec![];
|
let mut files = vec![];
|
||||||
@ -659,20 +688,7 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let category = creation.category;
|
let category = TORRENT_TAG_NAME.to_string();
|
||||||
|
|
||||||
if let Some(category) = category.as_deref() {
|
|
||||||
let has_caetgory = {
|
|
||||||
self.sync_data
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.categories
|
|
||||||
.contains_key(category)
|
|
||||||
};
|
|
||||||
if !has_caetgory {
|
|
||||||
self.add_category(category).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(source) = urls_source {
|
if let Some(source) = urls_source {
|
||||||
self.client
|
self.client
|
||||||
@ -680,8 +696,8 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
source,
|
source,
|
||||||
savepath: save_path.clone(),
|
savepath: save_path.clone(),
|
||||||
auto_torrent_management: Some(false),
|
auto_torrent_management: Some(false),
|
||||||
category: category.clone(),
|
category: Some(category.clone()),
|
||||||
tags: tags.clone(),
|
tags: tag.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@ -691,10 +707,10 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
self.client
|
self.client
|
||||||
.add_torrent(AddTorrentArg {
|
.add_torrent(AddTorrentArg {
|
||||||
source,
|
source,
|
||||||
savepath: save_path,
|
savepath: save_path.clone(),
|
||||||
auto_torrent_management: Some(false),
|
auto_torrent_management: Some(false),
|
||||||
category,
|
category: Some(category.clone()),
|
||||||
tags,
|
tags: tag,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@ -702,12 +718,12 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
self.wait_sync_until(
|
self.wait_sync_until(
|
||||||
|sync_data| {
|
|sync_data| {
|
||||||
let torrents = &sync_data.torrents;
|
let torrents = &sync_data.torrents;
|
||||||
hashes.iter().all(|hash| torrents.contains_key(hash))
|
ids.iter().all(|id| torrents.contains_key(id))
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(hashes)
|
Ok(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pause_downloads(
|
async fn pause_downloads(
|
||||||
@ -750,7 +766,6 @@ impl DownloaderTrait for QBittorrentDownloader {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let tasks = torrent_list
|
let tasks = torrent_list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(torrent_contents)
|
.zip(torrent_contents)
|
||||||
@ -812,8 +827,6 @@ impl Debug for QBittorrentDownloader {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
downloader::core::DownloadIdSelectorTrait,
|
downloader::core::DownloadIdSelectorTrait,
|
||||||
@ -829,47 +842,27 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct MockFileItem {
|
|
||||||
path: String,
|
|
||||||
size: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct MockRequest {
|
|
||||||
id: String,
|
|
||||||
file_list: Vec<MockFileItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct MockResponse {
|
|
||||||
torrent_url: String,
|
|
||||||
magnet_url: String,
|
|
||||||
hash: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "testcontainers")]
|
#[cfg(feature = "testcontainers")]
|
||||||
pub async fn create_torrents_testcontainers()
|
pub async fn create_torrents_testcontainers()
|
||||||
-> RResult<testcontainers::ContainerRequest<testcontainers::GenericImage>> {
|
-> RResult<testcontainers::ContainerRequest<testcontainers::GenericImage>> {
|
||||||
use testcontainers::{
|
use testcontainers::{
|
||||||
GenericImage,
|
GenericImage,
|
||||||
core::{ContainerPort, WaitFor},
|
core::{
|
||||||
|
ContainerPort,
|
||||||
|
// ReuseDirective,
|
||||||
|
WaitFor,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use testcontainers_modules::testcontainers::ImageExt;
|
use testcontainers_modules::testcontainers::ImageExt;
|
||||||
|
|
||||||
use crate::test_utils::testcontainers::ContainerRequestEnhancedExt;
|
use crate::test_utils::testcontainers::ContainerRequestEnhancedExt;
|
||||||
|
|
||||||
let container = GenericImage::new("ghcr.io/dumtruck/konobangu-testing-torrents", "latest")
|
let container = GenericImage::new("ghcr.io/dumtruck/konobangu-testing-torrents", "latest")
|
||||||
.with_wait_for(WaitFor::message_on_stdout("Listening on"))
|
.with_exposed_port()
|
||||||
.with_mapped_port(6080, ContainerPort::Tcp(6080))
|
.with_wait_for(WaitFor::message_on_stderr("Connection to localhost"))
|
||||||
.with_mapped_port(6081, ContainerPort::Tcp(6081))
|
|
||||||
.with_mapped_port(6082, ContainerPort::Tcp(6082))
|
|
||||||
// .with_reuse(ReuseDirective::Always)
|
// .with_reuse(ReuseDirective::Always)
|
||||||
.with_default_log_consumer()
|
.with_default_log_consumer()
|
||||||
.with_prune_existed_label("konobangu-testing-torrents", true, true)
|
.with_prune_existed_label("qbit-downloader", true, true)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(container)
|
Ok(container)
|
||||||
@ -924,36 +917,13 @@ pub mod tests {
|
|||||||
.with_test_writer()
|
.with_test_writer()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let torrents_image = create_torrents_testcontainers().await?;
|
let image = create_qbit_testcontainers().await?;
|
||||||
let _torrents_container = torrents_image.start().await?;
|
|
||||||
|
|
||||||
let torrents_req = MockRequest {
|
let container = image.start().await?;
|
||||||
id: "f10ebdda-dd2e-43f8-b80c-bf0884d071c4".into(),
|
|
||||||
file_list: vec![MockFileItem {
|
|
||||||
path: "[Nekomoe kissaten&LoliHouse] Boku no Kokoro no Yabai Yatsu - 20 [WebRip \
|
|
||||||
1080p HEVC-10bit AAC ASSx2].mkv"
|
|
||||||
.into(),
|
|
||||||
size: 1024,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
|
|
||||||
let torrent_res: MockResponse = reqwest::Client::new()
|
|
||||||
.post("http://127.0.0.1:6080/api/torrents/mock")
|
|
||||||
.json(&torrents_req)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let qbit_image = create_qbit_testcontainers().await?;
|
|
||||||
let qbit_container = qbit_image.start().await?;
|
|
||||||
|
|
||||||
let mut logs = String::new();
|
let mut logs = String::new();
|
||||||
|
|
||||||
qbit_container
|
container.stdout(false).read_to_string(&mut logs).await?;
|
||||||
.stdout(false)
|
|
||||||
.read_to_string(&mut logs)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let username = logs
|
let username = logs
|
||||||
.lines()
|
.lines()
|
||||||
@ -970,7 +940,7 @@ pub mod tests {
|
|||||||
let password = logs
|
let password = logs
|
||||||
.lines()
|
.lines()
|
||||||
.find_map(|line| {
|
.find_map(|line| {
|
||||||
if line.contains("A temporary password is provided for") {
|
if line.contains("A temporary password is provided for this session") {
|
||||||
line.split_whitespace().last()
|
line.split_whitespace().last()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -981,13 +951,7 @@ pub mod tests {
|
|||||||
|
|
||||||
tracing::info!(username, password);
|
tracing::info!(username, password);
|
||||||
|
|
||||||
test_qbittorrent_downloader_impl(
|
test_qbittorrent_downloader_impl(Some(username), Some(password)).await?;
|
||||||
torrent_res.torrent_url,
|
|
||||||
torrent_res.hash,
|
|
||||||
Some(username),
|
|
||||||
Some(password),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -1001,25 +965,31 @@ pub mod tests {
|
|||||||
let http_client = build_testing_http_client()?;
|
let http_client = build_testing_http_client()?;
|
||||||
let base_save_path = Path::new(get_tmp_qbit_test_folder());
|
let base_save_path = Path::new(get_tmp_qbit_test_folder());
|
||||||
|
|
||||||
let downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
let hash = "47ee2d69e7f19af783ad896541a07b012676f858".to_string();
|
||||||
|
|
||||||
|
let mut downloader = QBittorrentDownloader::from_creation(QBittorrentDownloaderCreation {
|
||||||
endpoint: "http://127.0.0.1:8080".to_string(),
|
endpoint: "http://127.0.0.1:8080".to_string(),
|
||||||
password: password.unwrap_or_default().to_string(),
|
password: password.unwrap_or_default().to_string(),
|
||||||
username: username.unwrap_or_default().to_string(),
|
username: username.unwrap_or_default().to_string(),
|
||||||
subscriber_id: 0,
|
subscriber_id: 0,
|
||||||
save_path: base_save_path.to_string(),
|
save_path: base_save_path.to_string(),
|
||||||
downloader_id: 0,
|
downloader_id: 0,
|
||||||
wait_sync_timeout: Some(Duration::from_secs(3)),
|
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
downloader.wait_sync_timeout = Duration::from_secs(3);
|
||||||
|
|
||||||
downloader.check_connection().await?;
|
downloader.check_connection().await?;
|
||||||
|
|
||||||
downloader
|
downloader
|
||||||
.remove_torrents(vec![torrent_hash.clone()].into())
|
.remove_torrents(vec![hash.clone()].into())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let torrent_source =
|
let torrent_source = HashTorrentSource::from_url_and_http_client(
|
||||||
HashTorrentSource::from_url_and_http_client(&http_client, torrent_url).await?;
|
&http_client,
|
||||||
|
format!("https://mikanani.me/Download/20240301/{}.torrent", &hash),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let folder_name = format!("torrent_test_{}", Utc::now().timestamp());
|
let folder_name = format!("torrent_test_{}", Utc::now().timestamp());
|
||||||
let save_path = base_save_path.join(&folder_name);
|
let save_path = base_save_path.join(&folder_name);
|
||||||
@ -1036,13 +1006,13 @@ pub mod tests {
|
|||||||
let get_torrent = async || -> Result<QBittorrentTask, DownloaderError> {
|
let get_torrent = async || -> Result<QBittorrentTask, DownloaderError> {
|
||||||
let torrent_infos = downloader
|
let torrent_infos = downloader
|
||||||
.query_downloads(QBittorrentSelector::Hash(QBittorrentHashSelector::from_id(
|
.query_downloads(QBittorrentSelector::Hash(QBittorrentHashSelector::from_id(
|
||||||
torrent_hash.clone(),
|
hash.clone(),
|
||||||
)))
|
)))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let result = torrent_infos
|
let result = torrent_infos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|t| t.hash_info() == torrent_hash)
|
.find(|t| t.hash_info() == hash)
|
||||||
.whatever_context::<_, DownloaderError>("no bittorrent")?;
|
.whatever_context::<_, DownloaderError>("no bittorrent")?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@ -1051,18 +1021,18 @@ pub mod tests {
|
|||||||
let target_torrent = get_torrent().await?;
|
let target_torrent = get_torrent().await?;
|
||||||
|
|
||||||
let files = target_torrent.contents;
|
let files = target_torrent.contents;
|
||||||
|
|
||||||
assert!(!files.is_empty());
|
assert!(!files.is_empty());
|
||||||
|
|
||||||
let first_file = files.first().expect("should have first file");
|
let first_file = files.first().expect("should have first file");
|
||||||
assert!(
|
assert_eq!(
|
||||||
&first_file.name.ends_with(r#"[Nekomoe kissaten&LoliHouse] Boku no Kokoro no Yabai Yatsu - 20 [WebRip 1080p HEVC-10bit AAC ASSx2].mkv"#)
|
&first_file.name,
|
||||||
|
r#"[Nekomoe kissaten&LoliHouse] Boku no Kokoro no Yabai Yatsu - 20 [WebRip 1080p HEVC-10bit AAC ASSx2].mkv"#
|
||||||
);
|
);
|
||||||
|
|
||||||
let test_tag = "test_tag".to_string();
|
let test_tag = format!("test_tag_{}", Utc::now().timestamp());
|
||||||
|
|
||||||
downloader
|
downloader
|
||||||
.add_torrent_tags(vec![torrent_hash.clone()], vec![test_tag.clone()])
|
.add_torrent_tags(vec![hash.clone()], vec![test_tag.clone()])
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let target_torrent = get_torrent().await?;
|
let target_torrent = get_torrent().await?;
|
||||||
@ -1072,7 +1042,7 @@ pub mod tests {
|
|||||||
let test_category = format!("test_category_{}", Utc::now().timestamp());
|
let test_category = format!("test_category_{}", Utc::now().timestamp());
|
||||||
|
|
||||||
downloader
|
downloader
|
||||||
.set_torrents_category(vec![torrent_hash.clone()], &test_category)
|
.set_torrents_category(vec![hash.clone()], &test_category)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let target_torrent = get_torrent().await?;
|
let target_torrent = get_torrent().await?;
|
||||||
@ -1085,7 +1055,7 @@ pub mod tests {
|
|||||||
let moved_torrent_path = base_save_path.join(format!("moved_{}", Utc::now().timestamp()));
|
let moved_torrent_path = base_save_path.join(format!("moved_{}", Utc::now().timestamp()));
|
||||||
|
|
||||||
downloader
|
downloader
|
||||||
.move_torrents(vec![torrent_hash.clone()], moved_torrent_path.as_str())
|
.move_torrents(vec![hash.clone()], moved_torrent_path.as_str())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let target_torrent = get_torrent().await?;
|
let target_torrent = get_torrent().await?;
|
||||||
@ -1103,7 +1073,30 @@ pub mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
downloader
|
downloader
|
||||||
.remove_torrents(vec![torrent_hash.clone()].into())
|
.move_torrent_contents(&hash, |f| {
|
||||||
|
f.replace(&folder_name, &format!("moved_{}", &folder_name))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let target_torrent = get_torrent().await?;
|
||||||
|
|
||||||
|
let actual_content_path = &target_torrent
|
||||||
|
.torrent
|
||||||
|
.content_path
|
||||||
|
.expect("failed to get actual content path");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
path_equals_as_file_url(
|
||||||
|
actual_content_path,
|
||||||
|
base_save_path.join(actual_content_path)
|
||||||
|
)
|
||||||
|
.whatever_context::<_, RError>(
|
||||||
|
"failed to compare actual content path and found expected content path"
|
||||||
|
)?
|
||||||
|
);
|
||||||
|
|
||||||
|
downloader
|
||||||
|
.remove_torrents(vec![hash.clone()].into())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let torrent_infos1 = downloader
|
let torrent_infos1 = downloader
|
||||||
@ -1116,8 +1109,6 @@ pub mod tests {
|
|||||||
|
|
||||||
assert!(torrent_infos1.is_empty());
|
assert!(torrent_infos1.is_empty());
|
||||||
|
|
||||||
tracing::info!("test finished");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,24 +79,16 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| TestcontainersError::Other(Box::new(error)))?;
|
.map_err(|error| TestcontainersError::Other(Box::new(error)))?;
|
||||||
|
|
||||||
if !remove_containers.is_empty() {
|
|
||||||
tracing::warn!(name = "stop running containers", result = ?remove_containers);
|
tracing::warn!(name = "stop running containers", result = ?remove_containers);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let result = client
|
let result = client
|
||||||
.prune_containers(Some(PruneContainersOptions { filters }))
|
.prune_containers(Some(PruneContainersOptions { filters }))
|
||||||
.await
|
.await
|
||||||
.map_err(|err| TestcontainersError::Other(Box::new(err)))?;
|
.map_err(|err| TestcontainersError::Other(Box::new(err)))?;
|
||||||
|
|
||||||
if result
|
|
||||||
.containers_deleted
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|c| !c.is_empty())
|
|
||||||
{
|
|
||||||
tracing::warn!(name = "prune existed containers", result = ?result);
|
tracing::warn!(name = "prune existed containers", result = ?result);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let result = self.with_labels([
|
let result = self.with_labels([
|
||||||
(TESTCONTAINERS_PRUNE_KEY, "true"),
|
(TESTCONTAINERS_PRUNE_KEY, "true"),
|
||||||
|
@ -8,14 +8,11 @@ FROM nodebt AS deps
|
|||||||
RUN mkdir -p /app/workspace
|
RUN mkdir -p /app/workspace
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json /app/
|
COPY package.json /app/
|
||||||
|
RUN pnpm approve-builds utf-8-validate node-datachannel utp-native
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile
|
||||||
|
|
||||||
FROM deps AS app
|
FROM deps AS app
|
||||||
|
|
||||||
COPY main.ts /app/
|
COPY main.ts /app/
|
||||||
|
|
||||||
EXPOSE 6080
|
|
||||||
EXPOSE 6081
|
|
||||||
EXPOSE 6082
|
|
||||||
|
|
||||||
CMD [ "npm", "start" ]
|
CMD [ "npm", "start" ]
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
# Konobangu Testing Torrents Container
|
# Konobangu Testing Torrents Container
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install --ignore-workspace
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -15,7 +9,7 @@ docker buildx build --platform linux/amd64 --tag konobangu-testing-torrents:late
|
|||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 6080:6080 -p 6081:6081 -p 6082:6082 --name konobangu-testing-torrents konobangu-testing-torrents:latest
|
docker run --network_mode=host --name konobangu-testing-torrents konobangu-testing-torrents:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publish
|
## Publish
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
konobangu-testing-torrents:
|
konobangu-testing-torrents:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
network_mode: host
|
||||||
- 6080:6080
|
|
||||||
- 6081:6081
|
|
||||||
- 6082:6082
|
|
||||||
container_name: konobangu-testing-torrents
|
container_name: konobangu-testing-torrents
|
||||||
|
@ -6,6 +6,7 @@ import fsp from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import TrackerServer from 'bittorrent-tracker/server';
|
import TrackerServer from 'bittorrent-tracker/server';
|
||||||
|
import createTorrent from 'create-torrent';
|
||||||
import WebTorrent, { type Torrent } from 'webtorrent';
|
import WebTorrent, { type Torrent } from 'webtorrent';
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
@ -16,7 +17,7 @@ const STATIC_API_PATH = '/api/static';
|
|||||||
const LOCAL_IP = '127.0.0.1';
|
const LOCAL_IP = '127.0.0.1';
|
||||||
const WORKSPACE_PATH = 'workspace';
|
const WORKSPACE_PATH = 'workspace';
|
||||||
const TRACKER_URL = `http://${LOCAL_IP}:${TRACKER_PORT}/announce`;
|
const TRACKER_URL = `http://${LOCAL_IP}:${TRACKER_PORT}/announce`;
|
||||||
const API_BASE_URL = `http://${LOCAL_IP}:${API_PORT}${STATIC_API_PATH}/`;
|
const API_BASE_URL = `http://${LOCAL_IP}:${API_PORT}/${STATIC_API_PATH}/`;
|
||||||
|
|
||||||
// Initialize Fastify instance
|
// Initialize Fastify instance
|
||||||
const app = Fastify({ logger: true });
|
const app = Fastify({ logger: true });
|
||||||
@ -28,9 +29,9 @@ app.register(fastifyStatic, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tracker = new TrackerServer({
|
const tracker = new TrackerServer({
|
||||||
udp: false, // enable udp server? [default=true]
|
udp: true, // enable udp server? [default=true]
|
||||||
http: true, // enable http server? [default=true]
|
http: true, // enable http server? [default=true]
|
||||||
ws: false, // enable websocket server? [default=true]
|
ws: true, // enable websocket server? [default=true]
|
||||||
stats: true, // enable web-based statistics? [default=true]
|
stats: true, // enable web-based statistics? [default=true]
|
||||||
trustProxy: true, // enable trusting x-forwarded-for header for remote IP [default=false]
|
trustProxy: true, // enable trusting x-forwarded-for header for remote IP [default=false]
|
||||||
});
|
});
|
||||||
@ -49,13 +50,12 @@ interface RequestSchema {
|
|||||||
interface ResponseSchema {
|
interface ResponseSchema {
|
||||||
torrentUrl: string;
|
torrentUrl: string;
|
||||||
magnetUrl: string;
|
magnetUrl: string;
|
||||||
hash: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start local Tracker
|
// Start local Tracker
|
||||||
async function startTracker(): Promise<void> {
|
async function startTracker(): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
tracker.listen(TRACKER_PORT, '0.0.0.0', () => {
|
tracker.listen(TRACKER_PORT, 'localhost', () => {
|
||||||
console.log(`Tracker listening on port ${TRACKER_PORT}`);
|
console.log(`Tracker listening on port ${TRACKER_PORT}`);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@ -85,26 +85,47 @@ async function generateMockFile(filePath: string, size: number) {
|
|||||||
await fsp.mkdir(dir, { recursive: true });
|
await fsp.mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsp.writeFile(filePath, Buffer.alloc(0));
|
|
||||||
await fsp.truncate(filePath, size);
|
await fsp.truncate(filePath, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add bittorrent and seed
|
// Generate bittorrent file
|
||||||
async function seedTorrent(
|
function generateTorrent(folderPath: string, torrentPath: string) {
|
||||||
torrentPath: string,
|
return new Promise<void>((resolve, reject) => {
|
||||||
contentFolder: string
|
createTorrent(
|
||||||
): Promise<Torrent> {
|
folderPath,
|
||||||
return new Promise((resolve) => {
|
|
||||||
const torrent = webTorrent.seed(
|
|
||||||
contentFolder,
|
|
||||||
{
|
{
|
||||||
announceList: [[TRACKER_URL]], // Specify tracker URL
|
announceList: [[TRACKER_URL]], // Specify tracker URL
|
||||||
private: false,
|
private: false,
|
||||||
createdBy: 'Konobangu Testing Torrents',
|
createdBy: 'WebTorrent',
|
||||||
|
comment: 'Generated by WebTorrent server',
|
||||||
urlList: [API_BASE_URL],
|
urlList: [API_BASE_URL],
|
||||||
},
|
},
|
||||||
async (t) => {
|
async (err, torrent) => {
|
||||||
await fsp.writeFile(torrentPath, t.torrentFile);
|
if (err) {
|
||||||
|
reject(new Error(`Failed to create torrent: ${err}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fsp.writeFile(torrentPath, torrent);
|
||||||
|
if (!fs.existsSync(torrentPath)) {
|
||||||
|
reject(new Error(`Torrent file ${torrentPath} was not created`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Generated torrent with tracker: ${TRACKER_URL}`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add bittorrent and seed
|
||||||
|
async function seedTorrent(torrentPath: string): Promise<Torrent> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const torrent = webTorrent.seed(
|
||||||
|
torrentPath,
|
||||||
|
{
|
||||||
|
announce: [TRACKER_URL],
|
||||||
|
},
|
||||||
|
(t) => {
|
||||||
resolve(t);
|
resolve(t);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -133,14 +154,14 @@ app.post<{ Body: RequestSchema }>('/api/torrents/mock', async (req, _reply) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const torrentPath = path.join(WORKSPACE_PATH, `${id}.torrent`);
|
const torrentPath = path.join(WORKSPACE_PATH, `${id}.torrent`);
|
||||||
|
await generateTorrent(idFolder, torrentPath);
|
||||||
|
|
||||||
const torrent = await seedTorrent(torrentPath, idFolder);
|
const torrent = await seedTorrent(torrentPath);
|
||||||
const magnetUrl = `magnet:?xt=urn:btih:${torrent.infoHash}&tr=${TRACKER_URL}`;
|
const magnetUrl = `magnet:?xt=urn:btih:${torrent.infoHash}&tr=${TRACKER_URL}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
torrentUrl: `${API_BASE_URL}${id}.torrent`,
|
torrentUrl: `${API_BASE_URL}${id}.torrent`,
|
||||||
magnetUrl,
|
magnetUrl,
|
||||||
hash: torrent.infoHash,
|
|
||||||
} as ResponseSchema;
|
} as ResponseSchema;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -148,8 +169,7 @@ app.post<{ Body: RequestSchema }>('/api/torrents/mock', async (req, _reply) => {
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
await startTracker();
|
await startTracker();
|
||||||
const address = await app.listen({ port: API_PORT, host: '0.0.0.0' });
|
await app.listen({ port: API_PORT, host: LOCAL_IP });
|
||||||
console.log('Listening on:', address);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Startup error:', err);
|
console.error('Startup error:', err);
|
||||||
webTorrent.destroy();
|
webTorrent.destroy();
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^8.1.1",
|
"@fastify/static": "^8.1.1",
|
||||||
"bittorrent-tracker": "^11.2.1",
|
"bittorrent-tracker": "^11.2.1",
|
||||||
|
"create-torrent": "^6.1.0",
|
||||||
"fastify": "^5.2.2",
|
"fastify": "^5.2.2",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"webtorrent": "^2.5.19"
|
"webtorrent": "^2.5.19"
|
||||||
@ -17,14 +18,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/create-torrent": "^5.0.2",
|
"@types/create-torrent": "^5.0.2",
|
||||||
"@types/webtorrent": "^0.110.0"
|
"@types/webtorrent": "^0.110.0"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"bufferutil",
|
|
||||||
"esbuild",
|
|
||||||
"node-datachannel",
|
|
||||||
"utf-8-validate",
|
|
||||||
"utp-native"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2458
packages/testing-torrents/pnpm-lock.yaml
generated
2458
packages/testing-torrents/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1567
pnpm-lock.yaml
generated
1567
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,3 @@
|
|||||||
packages:
|
packages:
|
||||||
- packages/*
|
- packages/*
|
||||||
- apps/*
|
- apps/*
|
||||||
- '!packages/testing-torrents'
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- '@biomejs/biome'
|
|
||||||
- bufferutil
|
|
||||||
- core-js
|
|
||||||
- esbuild
|
|
||||||
- sharp
|
|
||||||
- utf-8-validate
|
|
||||||
|
Loading…
Reference in New Issue
Block a user