diff --git a/Cargo.lock b/Cargo.lock
index 2980d02..ab4933b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4780,6 +4780,7 @@ dependencies = [
name = "recorder"
version = "0.1.0"
dependencies = [
+ "anyhow",
"async-graphql",
"async-graphql-axum",
"async-stream",
@@ -4793,6 +4794,7 @@ dependencies = [
"clap",
"cookie",
"ctor",
+ "dashmap 6.1.0",
"dotenv",
"fancy-regex",
"fastrand",
diff --git a/apps/recorder/.gitignore b/apps/recorder/.gitignore
index cacf1e1..b1a4dba 100644
--- a/apps/recorder/.gitignore
+++ b/apps/recorder/.gitignore
@@ -25,3 +25,4 @@ Cargo.lock
# Dist
node_modules
dist/
+temp/
diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml
index 640a745..a4c229c 100644
--- a/apps/recorder/Cargo.toml
+++ b/apps/recorder/Cargo.toml
@@ -131,6 +131,8 @@ ctor = "0.4.0"
librqbit = "8.0.0"
typed-builder = "0.21.0"
snafu = { version = "0.8.5", features = ["futures"] }
+anyhow = "1.0.97"
+dashmap = "6.1.0"
[dev-dependencies]
serial_test = "3"
insta = { version = "1", features = ["redactions", "yaml", "filters"] }
diff --git a/apps/recorder/examples/playground.rs b/apps/recorder/examples/playground.rs
index 7da4124..e78a529 100644
--- a/apps/recorder/examples/playground.rs
+++ b/apps/recorder/examples/playground.rs
@@ -1,4 +1,4 @@
-use recorder::errors::RResult;
+use recorder::errors::app_error::RResult;
// #![allow(unused_imports)]
// use recorder::{
// app::{AppContext, AppContextTrait},
diff --git a/apps/recorder/public/.gitkeep b/apps/recorder/public/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/apps/recorder/public/assets/404.html b/apps/recorder/public/assets/404.html
deleted file mode 100644
index dbd7df4..0000000
--- a/apps/recorder/public/assets/404.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- not found :-(
-
-
-
\ No newline at end of file
diff --git a/apps/recorder/src/app/builder.rs b/apps/recorder/src/app/builder.rs
index bd97546..e185e27 100644
--- a/apps/recorder/src/app/builder.rs
+++ b/apps/recorder/src/app/builder.rs
@@ -3,7 +3,7 @@ use std::sync::Arc;
use clap::{Parser, command};
use super::{AppContext, core::App, env::Environment};
-use crate::{app::config::AppConfig, errors::RResult};
+use crate::{app::config::AppConfig, errors::app_error::RResult};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
diff --git a/apps/recorder/src/app/config/mod.rs b/apps/recorder/src/app/config/mod.rs
index a996cea..5615841 100644
--- a/apps/recorder/src/app/config/mod.rs
+++ b/apps/recorder/src/app/config/mod.rs
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use super::env::Environment;
use crate::{
- auth::AuthConfig, cache::CacheConfig, database::DatabaseConfig, errors::RResult,
+ auth::AuthConfig, cache::CacheConfig, database::DatabaseConfig, errors::app_error::RResult,
extract::mikan::MikanConfig, graphql::GraphQLConfig, logger::LoggerConfig,
storage::StorageConfig, web::WebServerConfig,
};
diff --git a/apps/recorder/src/app/context.rs b/apps/recorder/src/app/context.rs
index 8a8d479..991f635 100644
--- a/apps/recorder/src/app/context.rs
+++ b/apps/recorder/src/app/context.rs
@@ -1,6 +1,6 @@
use super::{Environment, config::AppConfig};
use crate::{
- auth::AuthService, cache::CacheService, database::DatabaseService, errors::RResult,
+ auth::AuthService, cache::CacheService, database::DatabaseService, errors::app_error::RResult,
extract::mikan::MikanClient, graphql::GraphQLService, logger::LoggerService,
storage::StorageService,
};
diff --git a/apps/recorder/src/app/core.rs b/apps/recorder/src/app/core.rs
index 5b2abc0..f6ce2f7 100644
--- a/apps/recorder/src/app/core.rs
+++ b/apps/recorder/src/app/core.rs
@@ -6,7 +6,7 @@ use tokio::signal;
use super::{builder::AppBuilder, context::AppContextTrait};
use crate::{
- errors::RResult,
+ errors::app_error::RResult,
web::{
controller::{self, core::ControllerTrait},
middleware::default_middleware_stack,
diff --git a/apps/recorder/src/auth/oidc.rs b/apps/recorder/src/auth/oidc.rs
index 36b0472..12eb420 100644
--- a/apps/recorder/src/auth/oidc.rs
+++ b/apps/recorder/src/auth/oidc.rs
@@ -24,7 +24,9 @@ use super::{
errors::{AuthError, OidcProviderUrlSnafu, OidcRequestRedirectUriSnafu},
service::{AuthServiceTrait, AuthUserInfo},
};
-use crate::{app::AppContextTrait, errors::RError, fetch::HttpClient, models::auth::AuthType};
+use crate::{
+ app::AppContextTrait, errors::app_error::RError, fetch::HttpClient, models::auth::AuthType,
+};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OidcAuthClaims {
diff --git a/apps/recorder/src/bin/main.rs b/apps/recorder/src/bin/main.rs
index d2f87a1..3130dec 100644
--- a/apps/recorder/src/bin/main.rs
+++ b/apps/recorder/src/bin/main.rs
@@ -1,4 +1,4 @@
-use recorder::{app::AppBuilder, errors::RResult};
+use recorder::{app::AppBuilder, errors::app_error::RResult};
#[tokio::main]
async fn main() -> RResult<()> {
diff --git a/apps/recorder/src/cache/service.rs b/apps/recorder/src/cache/service.rs
index 717299c..0be477f 100644
--- a/apps/recorder/src/cache/service.rs
+++ b/apps/recorder/src/cache/service.rs
@@ -1,5 +1,5 @@
use super::CacheConfig;
-use crate::errors::RResult;
+use crate::errors::app_error::RResult;
pub struct CacheService {}
diff --git a/apps/recorder/src/database/service.rs b/apps/recorder/src/database/service.rs
index ebdde18..520e6e6 100644
--- a/apps/recorder/src/database/service.rs
+++ b/apps/recorder/src/database/service.rs
@@ -7,7 +7,7 @@ use sea_orm::{
use sea_orm_migration::MigratorTrait;
use super::DatabaseConfig;
-use crate::{errors::RResult, migrations::Migrator};
+use crate::{errors::app_error::RResult, migrations::Migrator};
pub struct DatabaseService {
connection: DatabaseConnection,
diff --git a/apps/recorder/src/downloader/bittorrent/downloader.rs b/apps/recorder/src/downloader/bittorrent/downloader.rs
new file mode 100644
index 0000000..0744305
--- /dev/null
+++ b/apps/recorder/src/downloader/bittorrent/downloader.rs
@@ -0,0 +1,77 @@
+use async_trait::async_trait;
+
+use crate::downloader::{
+ DownloaderError,
+ bittorrent::task::{
+ TorrentCreationTrait, TorrentHashTrait, TorrentStateTrait, TorrentTaskTrait,
+ },
+ core::{DownloadIdSelectorTrait, DownloadSelectorTrait, DownloadTaskTrait, DownloaderTrait},
+};
+
+#[async_trait]
+pub trait TorrentDownloaderTrait: DownloaderTrait
+where
+ Self::State: TorrentStateTrait,
+ Self::Id: TorrentHashTrait,
+ Self::Task: TorrentTaskTrait,
+ Self::Creation: TorrentCreationTrait,
+ Self::Selector: DownloadSelectorTrait,
+{
+ type IdSelector: DownloadIdSelectorTrait;
+
+ async fn pause_downloads(
+ &self,
+ selector: Self::Selector,
+ ) -> Result {
+ let hashes =
+ ::query_torrent_hashes(&self, selector).await?;
+ self.pause_torrents(hashes).await
+ }
+
+ async fn resume_downloads(
+ &self,
+ selector: Self::Selector,
+ ) -> Result {
+ let hashes =
+ ::query_torrent_hashes(&self, selector).await?;
+ self.resume_torrents(hashes).await
+ }
+ async fn remove_downloads(
+ &self,
+ selector: Self::Selector,
+ ) -> Result {
+ let hashes =
+ ::query_torrent_hashes(&self, selector).await?;
+ self.remove_torrents(hashes).await
+ }
+
+ async fn query_torrent_hashes(
+ &self,
+ selector: Self::Selector,
+ ) -> Result {
+ let hashes = match selector.try_into_ids_only() {
+ Ok(hashes) => Self::IdSelector::from_iter(hashes),
+ Err(selector) => {
+ let tasks = self.query_downloads(selector).await?;
+
+ Self::IdSelector::from_iter(tasks.into_iter().map(|s| s.into_id()))
+ }
+ };
+ Ok(hashes)
+ }
+
+ async fn pause_torrents(
+ &self,
+ hashes: Self::IdSelector,
+ ) -> Result;
+
+ async fn resume_torrents(
+ &self,
+ hashes: Self::IdSelector,
+ ) -> Result;
+
+ async fn remove_torrents(
+ &self,
+ hashes: Self::IdSelector,
+ ) -> Result;
+}
diff --git a/apps/recorder/src/downloader/bittorrent/mod.rs b/apps/recorder/src/downloader/bittorrent/mod.rs
new file mode 100644
index 0000000..05b1c62
--- /dev/null
+++ b/apps/recorder/src/downloader/bittorrent/mod.rs
@@ -0,0 +1,3 @@
+pub mod downloader;
+pub mod source;
+pub mod task;
diff --git a/apps/recorder/src/downloader/bittorrent/source.rs b/apps/recorder/src/downloader/bittorrent/source.rs
new file mode 100644
index 0000000..9c96fa7
--- /dev/null
+++ b/apps/recorder/src/downloader/bittorrent/source.rs
@@ -0,0 +1,228 @@
+use std::{
+ borrow::Cow,
+ fmt::{Debug, Formatter},
+};
+
+use bytes::Bytes;
+use librqbit_core::{magnet::Magnet, torrent_metainfo, torrent_metainfo::TorrentMetaV1Owned};
+use snafu::ResultExt;
+use url::Url;
+
+use crate::{
+ downloader::errors::{
+ DownloadFetchSnafu, DownloaderError, MagnetFormatSnafu, TorrentMetaSnafu,
+ },
+ errors::RAnyhowResultExt,
+ extract::bittorrent::core::MAGNET_SCHEMA,
+ fetch::{bytes::fetch_bytes, client::core::HttpClientTrait},
+};
+
+pub trait HashTorrentSourceTrait: Sized {
+ fn hash_info(&self) -> Cow<'_, str>;
+}
+
+pub struct MagnetUrlSource {
+ pub magnet: Magnet,
+ pub url: String,
+}
+
+impl MagnetUrlSource {
+ pub fn from_url(url: String) -> Result {
+ let magnet = Magnet::parse(&url)
+ .to_dyn_boxed()
+ .context(MagnetFormatSnafu {
+ message: url.clone(),
+ })?;
+
+ Ok(Self { magnet, url })
+ }
+}
+
+impl HashTorrentSourceTrait for MagnetUrlSource {
+ fn hash_info(&self) -> Cow<'_, str> {
+ let hash_info = self
+ .magnet
+ .as_id32()
+ .map(|s| s.as_string())
+ .or_else(|| self.magnet.as_id20().map(|s| s.as_string()))
+ .unwrap_or_else(|| unreachable!("hash of magnet must existed"));
+ hash_info.into()
+ }
+}
+
+impl Debug for MagnetUrlSource {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("MagnetUrlSource")
+ .field("url", &self.url)
+ .finish()
+ }
+}
+
+impl Clone for MagnetUrlSource {
+ fn clone(&self) -> Self {
+ Self {
+ magnet: Magnet::parse(&self.url).unwrap(),
+ url: self.url.clone(),
+ }
+ }
+}
+
+impl PartialEq for MagnetUrlSource {
+ fn eq(&self, other: &Self) -> bool {
+ self.url == other.url
+ }
+}
+
+impl Eq for MagnetUrlSource {}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TorrentUrlSource {
+ pub url: String,
+}
+
+impl TorrentUrlSource {
+ pub fn from_url(url: String) -> Result {
+ Ok(Self { url })
+ }
+}
+
+#[derive(Clone)]
+pub struct TorrentFileSource {
+ pub url: Option,
+ pub payload: Bytes,
+ pub meta: TorrentMetaV1Owned,
+ pub filename: String,
+}
+
+impl TorrentFileSource {
+ pub fn from_bytes(
+ filename: String,
+ bytes: Bytes,
+ url: Option,
+ ) -> Result {
+ let meta = torrent_metainfo::torrent_from_bytes(bytes.as_ref())
+ .to_dyn_boxed()
+ .with_context(|_| TorrentMetaSnafu {
+ message: format!(
+ "filename = {}, url = {}",
+ filename,
+ url.as_deref().unwrap_or_default()
+ ),
+ })?
+ .to_owned();
+
+ Ok(TorrentFileSource {
+ url,
+ payload: bytes,
+ meta,
+ filename,
+ })
+ }
+ pub async fn from_url_and_http_client(
+ client: &impl HttpClientTrait,
+ url: String,
+ ) -> Result {
+ let payload = fetch_bytes(client, &url)
+ .await
+ .boxed()
+ .with_context(|_| DownloadFetchSnafu { url: url.clone() })?;
+
+ let filename = Url::parse(&url)
+ .boxed()
+ .and_then(|s| {
+ s.path_segments()
+ .and_then(|p| p.last())
+ .map(String::from)
+ .ok_or_else(|| anyhow::anyhow!("invalid url"))
+ .to_dyn_boxed()
+ })
+ .with_context(|_| DownloadFetchSnafu { url: url.clone() })?;
+
+ Self::from_bytes(filename, payload, Some(url))
+ }
+}
+
+impl HashTorrentSourceTrait for TorrentFileSource {
+ fn hash_info(&self) -> Cow<'_, str> {
+ self.meta.info_hash.as_string().into()
+ }
+}
+
+impl Debug for TorrentFileSource {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("TorrentFileSource")
+ .field("hash", &self.meta.info_hash.as_string())
+ .finish()
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum UrlTorrentSource {
+ MagnetUrl(MagnetUrlSource),
+ TorrentUrl(TorrentUrlSource),
+}
+
+impl UrlTorrentSource {
+ pub fn from_url(url: String) -> Result {
+ let url_ = Url::parse(&url)?;
+ let source = if url_.scheme() == MAGNET_SCHEMA {
+ Self::from_magnet_url(url)?
+ } else {
+ Self::from_torrent_url(url)?
+ };
+ Ok(source)
+ }
+
+ pub fn from_magnet_url(url: String) -> Result {
+ let magnet_source = MagnetUrlSource::from_url(url)?;
+ Ok(Self::MagnetUrl(magnet_source))
+ }
+
+ pub fn from_torrent_url(url: String) -> Result {
+ let torrent_source = TorrentUrlSource::from_url(url)?;
+ Ok(Self::TorrentUrl(torrent_source))
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum HashTorrentSource {
+ MagnetUrl(MagnetUrlSource),
+ TorrentFile(TorrentFileSource),
+}
+
+impl HashTorrentSource {
+ pub async fn from_url_and_http_client(
+ client: &impl HttpClientTrait,
+ url: String,
+ ) -> Result {
+ let url_ = Url::parse(&url)?;
+ let source = if url_.scheme() == MAGNET_SCHEMA {
+ Self::from_magnet_url(url)?
+ } else {
+ Self::from_torrent_url_and_http_client(client, url).await?
+ };
+ Ok(source)
+ }
+
+ pub fn from_magnet_url(url: String) -> Result {
+ let magnet_source = MagnetUrlSource::from_url(url)?;
+ Ok(Self::MagnetUrl(magnet_source))
+ }
+
+ pub async fn from_torrent_url_and_http_client(
+ client: &impl HttpClientTrait,
+ url: String,
+ ) -> Result {
+ let torrent_source = TorrentFileSource::from_url_and_http_client(client, url).await?;
+ Ok(Self::TorrentFile(torrent_source))
+ }
+}
+
+impl HashTorrentSourceTrait for HashTorrentSource {
+ fn hash_info(&self) -> Cow<'_, str> {
+ match self {
+ HashTorrentSource::MagnetUrl(m) => m.hash_info(),
+ HashTorrentSource::TorrentFile(t) => t.hash_info(),
+ }
+ }
+}
diff --git a/apps/recorder/src/downloader/bittorrent/task.rs b/apps/recorder/src/downloader/bittorrent/task.rs
new file mode 100644
index 0000000..4da7a15
--- /dev/null
+++ b/apps/recorder/src/downloader/bittorrent/task.rs
@@ -0,0 +1,37 @@
+use std::{borrow::Cow, hash::Hash};
+
+use quirks_path::{Path, PathBuf};
+
+use crate::downloader::{
+ bittorrent::source::HashTorrentSource,
+ core::{DownloadCreationTrait, DownloadIdTrait, DownloadStateTrait, DownloadTaskTrait},
+};
+
+pub const TORRENT_TAG_NAME: &str = "konobangu";
+
+pub trait TorrentHashTrait: DownloadIdTrait + Send + Hash {}
+
+pub trait TorrentStateTrait: DownloadStateTrait {}
+
+pub trait TorrentTaskTrait: DownloadTaskTrait
+where
+ Self::State: TorrentStateTrait,
+ Self::Id: TorrentHashTrait,
+{
+ fn hash_info(&self) -> &str;
+ fn name(&self) -> Cow<'_, str> {
+ Cow::Borrowed(self.hash_info())
+ }
+
+ fn tags(&self) -> impl Iterator- >;
+
+ fn category(&self) -> Option>;
+}
+
+pub trait TorrentCreationTrait: DownloadCreationTrait {
+ fn save_path(&self) -> &Path;
+
+ fn save_path_mut(&mut self) -> &mut PathBuf;
+
+ fn sources_mut(&mut self) -> &mut Vec;
+}
diff --git a/apps/recorder/src/downloader/core.rs b/apps/recorder/src/downloader/core.rs
index b32f837..8c07d8e 100644
--- a/apps/recorder/src/downloader/core.rs
+++ b/apps/recorder/src/downloader/core.rs
@@ -1,297 +1,218 @@
-use std::fmt::Debug;
+use std::{
+ any::Any, borrow::Cow, fmt::Debug, hash::Hash, marker::PhantomData, ops::Deref, time::Duration,
+ vec::IntoIter,
+};
use async_trait::async_trait;
-use itertools::Itertools;
-use lazy_static::lazy_static;
-use librqbit_core::{
- magnet::Magnet,
- torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes},
-};
-use quirks_path::{Path, PathBuf};
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use snafu::prelude::*;
-use url::Url;
-use super::{DownloaderError, QbitTorrent, QbitTorrentContent, errors::DownloadFetchSnafu};
-use crate::fetch::{HttpClientTrait, fetch_bytes};
+use super::DownloaderError;
-pub const BITTORRENT_MIME_TYPE: &str = "application/x-bittorrent";
-pub const MAGNET_SCHEMA: &str = "magnet";
+pub trait DownloadStateTrait: Sized + Debug {}
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum TorrentFilter {
- All,
- Downloading,
- Completed,
- Paused,
- Active,
- Inactive,
- Resumed,
- Stalled,
- StalledUploading,
- StalledDownloading,
- Errored,
-}
+pub trait DownloadIdTrait: Hash + Sized + Clone + Send + Debug {}
-lazy_static! {
- static ref TORRENT_HASH_RE: Regex = Regex::new(r"[a-fA-F0-9]{40}").unwrap();
- static ref TORRENT_EXT_RE: Regex = Regex::new(r"\.torrent$").unwrap();
-}
+pub trait DownloadTaskTrait: Sized + Send + Debug {
+ type State: DownloadStateTrait;
+ type Id: DownloadIdTrait;
-#[derive(Clone, PartialEq, Eq)]
-pub enum TorrentSource {
- MagnetUrl {
- url: Url,
- hash: String,
- },
- TorrentUrl {
- url: Url,
- hash: String,
- },
- TorrentFile {
- torrent: Vec,
- hash: String,
- name: Option,
- },
-}
-
-impl TorrentSource {
- pub async fn parse(client: &H, url: &str) -> Result {
- let url = Url::parse(url)?;
- let source = if url.scheme() == MAGNET_SCHEMA {
- TorrentSource::from_magnet_url(url)?
- } else if let Some(basename) = url
- .clone()
- .path_segments()
- .and_then(|mut segments| segments.next_back())
- {
- if let (Some(match_hash), true) = (
- TORRENT_HASH_RE.find(basename),
- TORRENT_EXT_RE.is_match(basename),
- ) {
- TorrentSource::from_torrent_url(url, match_hash.as_str().to_string())?
+ fn id(&self) -> &Self::Id;
+ fn into_id(self) -> Self::Id;
+ fn name(&self) -> Cow<'_, str>;
+ fn speed(&self) -> Option;
+ fn state(&self) -> &Self::State;
+ fn dl_bytes(&self) -> Option;
+ fn total_bytes(&self) -> Option;
+ fn left_bytes(&self) -> Option {
+ if let (Some(tt), Some(dl)) = (self.total_bytes(), self.dl_bytes()) {
+ tt.checked_sub(dl)
+ } else {
+ None
+ }
+ }
+ fn et(&self) -> Option;
+ fn eta(&self) -> Option {
+ if let (Some(left_bytes), Some(speed)) = (self.left_bytes(), self.speed()) {
+ if speed > 0 {
+ Some(Duration::from_secs_f64(left_bytes as f64 / speed as f64))
} else {
- let contents = fetch_bytes(client, url)
- .await
- .boxed()
- .context(DownloadFetchSnafu)?;
- TorrentSource::from_torrent_file(contents.to_vec(), Some(basename.to_string()))?
+ None
}
} else {
- let contents = fetch_bytes(client, url)
- .await
- .boxed()
- .context(DownloadFetchSnafu)?;
- TorrentSource::from_torrent_file(contents.to_vec(), None)?
- };
- Ok(source)
+ None
+ }
}
+ fn average_speed(&self) -> Option {
+ if let (Some(et), Some(dl_bytes)) = (self.et(), self.dl_bytes()) {
+ let secs = et.as_secs_f64();
- pub fn from_torrent_file(file: Vec, name: Option) -> Result {
- let torrent: TorrentMetaV1Owned =
- torrent_from_bytes(&file).map_err(|_| DownloaderError::TorrentFileFormatError)?;
- let hash = torrent.info_hash.as_string();
- Ok(TorrentSource::TorrentFile {
- torrent: file,
- hash,
- name,
- })
- }
-
- pub fn from_magnet_url(url: Url) -> Result {
- if url.scheme() != MAGNET_SCHEMA {
- Err(DownloaderError::DownloadSchemaError {
- found: url.scheme().to_string(),
- expected: MAGNET_SCHEMA.to_string(),
- })
+ if secs > 0.0 {
+ Some(dl_bytes as f64 / secs)
+ } else {
+ None
+ }
} else {
- let magnet =
- Magnet::parse(url.as_str()).map_err(|_| DownloaderError::MagnetFormatError {
- url: url.as_str().to_string(),
- })?;
-
- let hash = magnet
- .as_id20()
- .ok_or_else(|| DownloaderError::MagnetFormatError {
- url: url.as_str().to_string(),
- })?
- .as_string();
- Ok(TorrentSource::MagnetUrl { url, hash })
+ None
}
}
-
- pub fn from_torrent_url(url: Url, hash: String) -> Result {
- Ok(TorrentSource::TorrentUrl { url, hash })
- }
-
- pub fn hash(&self) -> &str {
- match self {
- TorrentSource::MagnetUrl { hash, .. } => hash,
- TorrentSource::TorrentUrl { hash, .. } => hash,
- TorrentSource::TorrentFile { hash, .. } => hash,
- }
- }
-}
-
-impl Debug for TorrentSource {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- TorrentSource::MagnetUrl { url, .. } => {
- write!(f, "MagnetUrl {{ url: {} }}", url.as_str())
+ fn progress(&self) -> Option {
+ if let (Some(dl), Some(tt)) = (self.dl_bytes(), self.total_bytes()) {
+ if dl > 0 {
+ if tt > 0 {
+ Some(dl as f32 / tt as f32)
+ } else {
+ None
+ }
+ } else {
+ Some(0.0)
}
- TorrentSource::TorrentUrl { url, .. } => {
- write!(f, "TorrentUrl {{ url: {} }}", url.as_str())
- }
- TorrentSource::TorrentFile { name, hash, .. } => write!(
- f,
- "TorrentFile {{ name: \"{}\", hash: \"{hash}\" }}",
- name.as_deref().unwrap_or_default()
- ),
+ } else {
+ None
}
}
}
-pub trait TorrentContent {
- fn get_name(&self) -> &str;
-
- fn get_all_size(&self) -> u64;
-
- fn get_progress(&self) -> f64;
-
- fn get_curr_size(&self) -> u64;
+pub trait DownloadCreationTrait: Sized {
+ type Task: DownloadTaskTrait;
}
-impl TorrentContent for QbitTorrentContent {
- fn get_name(&self) -> &str {
- self.name.as_str()
- }
+pub trait DownloadSelectorTrait: Sized + Any + Send {
+ type Id: DownloadIdTrait;
+ type Task: DownloadTaskTrait;
- fn get_all_size(&self) -> u64 {
- self.size
- }
-
- fn get_progress(&self) -> f64 {
- self.progress
- }
-
- fn get_curr_size(&self) -> u64 {
- u64::clamp(
- f64::round(self.get_all_size() as f64 * self.get_progress()) as u64,
- 0,
- self.get_all_size(),
- )
+ fn try_into_ids_only(self) -> Result, Self> {
+ Err(self)
}
}
-#[derive(Debug, Clone)]
-pub enum Torrent {
- Qbit {
- torrent: QbitTorrent,
- contents: Vec,
- },
+pub trait DownloadIdSelectorTrait:
+ DownloadSelectorTrait
+ + IntoIterator
-
+ + FromIterator
+ + Into>
+ + From>
+{
+ fn try_into_ids_only(self) -> Result, Self> {
+ Ok(Vec::from_iter(self))
+ }
+
+ fn from_id(id: Self::Id) -> Self;
}
-impl Torrent {
- pub fn iter_files(&self) -> impl Iterator
- {
- match self {
- Torrent::Qbit { contents, .. } => {
- contents.iter().map(|item| item as &dyn TorrentContent)
- }
+#[derive(Debug)]
+pub struct DownloadIdSelector
+where
+ Task: DownloadTaskTrait,
+{
+ pub ids: Vec,
+ pub marker: PhantomData,
+}
+
+impl Deref for DownloadIdSelector
+where
+ Task: DownloadTaskTrait,
+{
+ type Target = Vec;
+
+ fn deref(&self) -> &Self::Target {
+ &self.ids
+ }
+}
+
+impl IntoIterator for DownloadIdSelector
+where
+ Task: DownloadTaskTrait,
+{
+ type Item = Task::Id;
+ type IntoIter = IntoIter;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.ids.into_iter()
+ }
+}
+
+impl FromIterator for DownloadIdSelector
+where
+ Task: DownloadTaskTrait,
+{
+ fn from_iter>(iter: T) -> Self {
+ Self {
+ ids: Vec::from_iter(iter),
+ marker: PhantomData,
}
}
+}
- pub fn get_name(&self) -> Option<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.name.as_deref(),
+impl DownloadSelectorTrait for DownloadIdSelector
+where
+ Task: DownloadTaskTrait + 'static,
+{
+ type Id = Task::Id;
+ type Task = Task;
+}
+
+impl From> for DownloadIdSelector
+where
+ Task: DownloadTaskTrait + 'static,
+{
+ fn from(value: Vec) -> Self {
+ Self {
+ ids: value,
+ marker: PhantomData,
}
}
+}
- pub fn get_hash(&self) -> Option<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.hash.as_deref(),
- }
+impl From> for Vec
+where
+ Task: DownloadTaskTrait + 'static,
+{
+ fn from(value: DownloadIdSelector) -> Self {
+ value.ids
+ }
+}
+
+impl DownloadIdSelectorTrait for DownloadIdSelector
+where
+ Task: DownloadTaskTrait + 'static,
+{
+ fn try_into_ids_only(self) -> Result, Self> {
+ Ok(self.ids)
}
- pub fn get_save_path(&self) -> Option<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.save_path.as_deref(),
- }
- }
-
- pub fn get_content_path(&self) -> Option<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.content_path.as_deref(),
- }
- }
-
- pub fn get_tags(&self) -> Vec<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.tags.as_deref().map_or_else(Vec::new, |s| {
- s.split(',')
- .map(|s| s.trim())
- .filter(|s| !s.is_empty())
- .collect_vec()
- }),
- }
- }
-
- pub fn get_category(&self) -> Option<&str> {
- match self {
- Torrent::Qbit { torrent, .. } => torrent.category.as_deref(),
+ fn from_id(id: Self::Id) -> Self {
+ Self {
+ ids: vec![id],
+ marker: PhantomData,
}
}
}
#[async_trait]
-pub trait TorrentDownloader {
- async fn get_torrents_info(
+pub trait DownloaderTrait {
+ type State: DownloadStateTrait;
+ type Id: DownloadIdTrait;
+ type Task: DownloadTaskTrait;
+ type Creation: DownloadCreationTrait;
+ type Selector: DownloadSelectorTrait;
+
+ async fn add_downloads(
&self,
- status_filter: TorrentFilter,
- category: Option,
- tag: Option,
- ) -> Result, DownloaderError>;
-
- async fn add_torrents(
+ creation: Self::Creation,
+ ) -> Result, DownloaderError>;
+ async fn pause_downloads(
&self,
- source: TorrentSource,
- save_path: String,
- category: Option<&str>,
- ) -> Result<(), DownloaderError>;
-
- async fn delete_torrents(&self, hashes: Vec) -> Result<(), DownloaderError>;
-
- async fn rename_torrent_file(
+ selector: Self::Selector,
+ ) -> Result, DownloaderError>;
+ async fn resume_downloads(
&self,
- hash: &str,
- old_path: &str,
- new_path: &str,
- ) -> Result<(), DownloaderError>;
-
- async fn move_torrents(
+ selector: Self::Selector,
+ ) -> Result, DownloaderError>;
+ async fn remove_downloads(
&self,
- hashes: Vec,
- new_path: &str,
- ) -> Result<(), DownloaderError>;
-
- async fn get_torrent_path(&self, hashes: String) -> Result