Compare commits

..

No commits in common. "a3609696c7f930c754d6d6e99aea2aeace061a6e" and "ecb56013a5f11138d3cf52a6f3b09787f5dc9bcd" have entirely different histories.

12 changed files with 1734 additions and 2673 deletions

View File

@ -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
} }

View File

@ -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()

View File

@ -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; if !sync_data.categories.contains_key(category) {
!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(())
} }
} }

View File

@ -79,9 +79,7 @@ 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
@ -89,13 +87,7 @@ where
.await .await
.map_err(|err| TestcontainersError::Other(Box::new(err)))?; .map_err(|err| TestcontainersError::Other(Box::new(err)))?;
if result tracing::warn!(name = "prune existed containers", result = ?result);
.containers_deleted
.as_ref()
.is_some_and(|c| !c.is_empty())
{
tracing::warn!(name = "prune existed containers", result = ?result);
}
} }
let result = self.with_labels([ let result = self.with_labels([

View File

@ -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" ]

View File

@ -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
@ -23,4 +17,4 @@ docker run -p 6080:6080 -p 6081:6081 -p 6082:6082 --name konobangu-testing-torre
```bash ```bash
docker tag konobangu-testing-torrents:latest ghcr.io/dumtruck/konobangu-testing-torrents:latest docker tag konobangu-testing-torrents:latest ghcr.io/dumtruck/konobangu-testing-torrents:latest
docker push ghcr.io/dumtruck/konobangu-testing-torrents:latest docker push ghcr.io/dumtruck/konobangu-testing-torrents:latest
``` ```

View File

@ -1,8 +1,5 @@
services: services:
konobangu-testing-torrents: konobangu-testing-torrents:
build: . build: .
ports: network_mode: host
- 6080:6080 container_name: konobangu-testing-torrents
- 6081:6081
- 6082:6082
container_name: konobangu-testing-torrents

View File

@ -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();

View File

@ -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"
]
} }
} }

File diff suppressed because it is too large Load Diff

1567
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,3 @@
packages: packages:
- packages/* - packages/*
- apps/* - apps/*
- '!packages/testing-torrents'
onlyBuiltDependencies:
- '@biomejs/biome'
- bufferutil
- core-js
- esbuild
- sharp
- utf-8-validate