feat(downloader): add rqbit impl
This commit is contained in:
parent
2686fa1d76
commit
1ff8a311ae
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -1534,6 +1534,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dashmap 6.1.0",
|
||||
"fetch",
|
||||
"futures",
|
||||
"itertools 0.14.0",
|
||||
@ -3976,9 +3977,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "opendal"
|
||||
version = "0.51.2"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1063ea459fa9e94584115743b06330f437902dd1d9f692b863ef1875a20548"
|
||||
checksum = "b5ebd1183902124c6b3ee0a9383683513dd8cca3d25a5d065593f969a44f979e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -3991,7 +3992,6 @@ dependencies = [
|
||||
"http",
|
||||
"log",
|
||||
"md-5",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"quick-xml 0.36.2",
|
||||
"reqwest",
|
||||
@ -4912,6 +4912,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"snafu",
|
||||
"string-interner",
|
||||
"tera",
|
||||
"testcontainers",
|
||||
"testcontainers-ext",
|
||||
@ -5095,9 +5096,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e8975513bd9a7a43aad01030e79b3498e05db14e9d945df6483e8cf9b8c4c4"
|
||||
checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -5132,9 +5133,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-tracing"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c88a8d9cfe3319b5adc10f3ffc3db75c7346837a1f857f8269f6361f3b2744"
|
||||
checksum = "d75b0eee96990cfb4c09545847385e89b2d2d2e571143d55264a05d77c713780"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -6353,6 +6354,16 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766"
|
||||
|
||||
[[package]]
|
||||
name = "string-interner"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
@ -7283,7 +7294,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7927,9 +7940,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.4"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
|
||||
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -22,7 +22,7 @@ tokio = { version = "1", features = ["macros", "fs", "rt-multi-thread"] }
|
||||
serde_json = "1"
|
||||
async-trait = "0.1"
|
||||
tracing = "0.1"
|
||||
url = "2.5"
|
||||
url = "2.5.2"
|
||||
anyhow = "1"
|
||||
itertools = "0.14"
|
||||
chrono = "0.4"
|
||||
|
@ -61,15 +61,15 @@ sea-orm-migration = { version = "1.1", features = ["runtime-tokio-rustls"] }
|
||||
rss = "2"
|
||||
fancy-regex = "0.14"
|
||||
maplit = "1.0.2"
|
||||
lightningcss = "1.0.0-alpha.61"
|
||||
lightningcss = "1.0.0-alpha.65"
|
||||
html-escape = "0.2.13"
|
||||
opendal = { version = "0.51.0", features = ["default", "services-fs"] }
|
||||
opendal = { version = "0.53", features = ["default", "services-fs"] }
|
||||
zune-image = "0.4.15"
|
||||
once_cell = "1.20.2"
|
||||
scraper = "0.23"
|
||||
|
||||
jwt-authorizer = "0.15.0"
|
||||
log = "0.4.22"
|
||||
log = "0.4"
|
||||
async-graphql = { version = "7", features = [] }
|
||||
async-graphql-axum = "7"
|
||||
seaography = { version = "1.1" }
|
||||
@ -100,6 +100,7 @@ serde_yaml = "0.9.34"
|
||||
downloader = { workspace = true }
|
||||
util = { workspace = true }
|
||||
fetch = { workspace = true }
|
||||
string-interner = "0.19.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3"
|
||||
|
@ -35,7 +35,7 @@ use crate::{app::AppContextTrait, errors::RecorderError, models::auth::AuthType}
|
||||
|
||||
pub struct OidcHttpClient(pub Arc<HttpClient>);
|
||||
|
||||
impl<'a> Deref for OidcHttpClient {
|
||||
impl Deref for OidcHttpClient {
|
||||
type Target = HttpClient;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -170,8 +170,8 @@ pub struct OidcAuthService {
|
||||
}
|
||||
|
||||
impl OidcAuthService {
|
||||
pub async fn build_authorization_request<'a>(
|
||||
&'a self,
|
||||
pub async fn build_authorization_request(
|
||||
&self,
|
||||
redirect_uri: &str,
|
||||
) -> Result<OidcAuthRequest, AuthError> {
|
||||
let oidc_provider_client = OidcHttpClient(self.oidc_provider_client.clone());
|
||||
@ -247,8 +247,8 @@ impl OidcAuthService {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn extract_authorization_request_callback<'a>(
|
||||
&'a self,
|
||||
pub async fn extract_authorization_request_callback(
|
||||
&self,
|
||||
query: OidcAuthCallbackQuery,
|
||||
) -> Result<OidcAuthCallbackPayload, AuthError> {
|
||||
let oidc_http_client = OidcHttpClient(self.oidc_provider_client.clone());
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -1,5 +1,4 @@
|
||||
pub mod app_error;
|
||||
pub mod ext;
|
||||
pub mod response;
|
||||
|
||||
pub use app_error::{RecorderError, RecorderResult};
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{fmt::Debug, ops::Deref};
|
||||
|
||||
use fetch::{FetchError, HttpClient, HttpClientTrait, client::HttpClientCookiesAuth};
|
||||
use fetch::{HttpClient, HttpClientTrait, client::HttpClientCookiesAuth};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
@ -24,7 +24,6 @@ impl Debug for MikanAuthSecrecy {
|
||||
impl MikanAuthSecrecy {
|
||||
pub fn into_cookie_auth(self, url: &Url) -> Result<HttpClientCookiesAuth, RecorderError> {
|
||||
HttpClientCookiesAuth::from_cookies(&self.cookie, url, self.user_agent)
|
||||
.map_err(FetchError::from)
|
||||
.map_err(RecorderError::from)
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ librqbit = { version = "8", features = ["async-bt", "watch"] }
|
||||
util = { workspace = true }
|
||||
testing-torrents = { workspace = true, optional = true }
|
||||
fetch = { workspace = true }
|
||||
dashmap = "6.1.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -24,9 +24,10 @@ where
|
||||
Self::State: TorrentStateTrait,
|
||||
Self::Id: TorrentHashTrait,
|
||||
{
|
||||
fn hash_info(&self) -> &str;
|
||||
fn hash_info(&self) -> Cow<'_, str>;
|
||||
|
||||
fn name(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(self.hash_info())
|
||||
self.hash_info()
|
||||
}
|
||||
|
||||
fn tags(&self) -> impl Iterator<Item = Cow<'_, str>>;
|
||||
|
@ -7,7 +7,18 @@ use async_trait::async_trait;
|
||||
|
||||
use super::DownloaderError;
|
||||
|
||||
pub trait DownloadStateTrait: Sized + Debug {}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DownloadSimpleState {
|
||||
Paused,
|
||||
Active,
|
||||
Completed,
|
||||
Error,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub trait DownloadStateTrait: Sized + Debug {
|
||||
fn to_download_state(&self) -> DownloadSimpleState;
|
||||
}
|
||||
|
||||
pub trait DownloadIdTrait: Hash + Sized + Clone + Send + Debug {}
|
||||
|
||||
|
@ -35,6 +35,11 @@ pub enum DownloaderError {
|
||||
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))]
|
||||
source: OptDynErr,
|
||||
},
|
||||
#[snafu(display("{source}"))]
|
||||
RqbitError {
|
||||
#[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, OptDynErr::some)))]
|
||||
source: OptDynErr,
|
||||
},
|
||||
#[snafu(display("{message}"))]
|
||||
Whatever {
|
||||
message: String,
|
||||
|
@ -17,7 +17,7 @@ use qbit_rs::{
|
||||
Torrent as QbitTorrent, TorrentFile, TorrentSource,
|
||||
},
|
||||
};
|
||||
use quirks_path::{Path, PathBuf};
|
||||
use quirks_path::PathBuf;
|
||||
use snafu::{OptionExt, whatever};
|
||||
use tokio::{
|
||||
sync::{RwLock, watch},
|
||||
@ -26,6 +26,7 @@ use tokio::{
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
use super::QBittorrentHashSelector;
|
||||
use crate::{
|
||||
DownloaderError,
|
||||
bittorrent::{
|
||||
@ -33,7 +34,7 @@ use crate::{
|
||||
source::{HashTorrentSource, HashTorrentSourceTrait, MagnetUrlSource, TorrentFileSource},
|
||||
task::TORRENT_TAG_NAME,
|
||||
},
|
||||
core::{DownloadIdSelector, DownloaderTrait},
|
||||
core::DownloaderTrait,
|
||||
qbit::task::{
|
||||
QBittorrentCreation, QBittorrentHash, QBittorrentSelector, QBittorrentState,
|
||||
QBittorrentTask,
|
||||
@ -41,6 +42,7 @@ use crate::{
|
||||
utils::path_equals_as_file_url,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QBittorrentDownloaderCreation {
|
||||
pub endpoint: String,
|
||||
pub username: String,
|
||||
@ -130,6 +132,7 @@ pub struct QBittorrentDownloader {
|
||||
}
|
||||
|
||||
impl QBittorrentDownloader {
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn from_creation(
|
||||
creation: QBittorrentDownloaderCreation,
|
||||
) -> Result<Arc<Self>, DownloaderError> {
|
||||
@ -253,10 +256,6 @@ impl QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_save_path(&self, sub_path: &Path) -> PathBuf {
|
||||
self.save_path.join(sub_path)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn add_torrent_tags(
|
||||
&self,
|
||||
@ -324,6 +323,7 @@ impl QBittorrentDownloader {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
pub async fn get_torrent_path(
|
||||
&self,
|
||||
hashes: String,
|
||||
@ -406,6 +406,7 @@ impl DownloaderTrait for QBittorrentDownloader {
|
||||
type Creation = QBittorrentCreation;
|
||||
type Selector = QBittorrentSelector;
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn add_downloads(
|
||||
&self,
|
||||
creation: <Self as DownloaderTrait>::Creation,
|
||||
@ -524,6 +525,7 @@ impl DownloaderTrait for QBittorrentDownloader {
|
||||
<Self as TorrentDownloaderTrait>::remove_downloads(self, selector).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn query_downloads(
|
||||
&self,
|
||||
selector: QBittorrentSelector,
|
||||
@ -555,13 +557,13 @@ impl DownloaderTrait for QBittorrentDownloader {
|
||||
|
||||
#[async_trait]
|
||||
impl TorrentDownloaderTrait for QBittorrentDownloader {
|
||||
type IdSelector = DownloadIdSelector<Self::Task>;
|
||||
type IdSelector = QBittorrentHashSelector;
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn pause_torrents(
|
||||
&self,
|
||||
hashes: <Self as TorrentDownloaderTrait>::IdSelector,
|
||||
) -> Result<<Self as TorrentDownloaderTrait>::IdSelector, DownloaderError> {
|
||||
) -> Result<Self::IdSelector, DownloaderError> {
|
||||
self.client.pause_torrents(hashes.clone()).await?;
|
||||
Ok(hashes)
|
||||
}
|
||||
@ -579,7 +581,7 @@ impl TorrentDownloaderTrait for QBittorrentDownloader {
|
||||
async fn remove_torrents(
|
||||
&self,
|
||||
hashes: <Self as TorrentDownloaderTrait>::IdSelector,
|
||||
) -> Result<<Self as TorrentDownloaderTrait>::IdSelector, DownloaderError> {
|
||||
) -> Result<Self::IdSelector, DownloaderError> {
|
||||
self.client
|
||||
.delete_torrents(hashes.clone(), Some(true))
|
||||
.await?;
|
||||
|
@ -13,8 +13,8 @@ use crate::{
|
||||
task::{SimpleTorrentHash, TorrentCreationTrait, TorrentStateTrait, TorrentTaskTrait},
|
||||
},
|
||||
core::{
|
||||
DownloadCreationTrait, DownloadIdSelector, DownloadSelectorTrait, DownloadStateTrait,
|
||||
DownloadTaskTrait,
|
||||
DownloadCreationTrait, DownloadIdSelector, DownloadSelectorTrait, DownloadSimpleState,
|
||||
DownloadStateTrait, DownloadTaskTrait,
|
||||
},
|
||||
};
|
||||
|
||||
@ -35,7 +35,34 @@ impl From<Option<State>> for QBittorrentState {
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadStateTrait for QBittorrentState {}
|
||||
impl DownloadStateTrait for QBittorrentState {
|
||||
fn to_download_state(&self) -> DownloadSimpleState {
|
||||
if let Some(ref state) = self.0 {
|
||||
match state {
|
||||
State::ForcedUP
|
||||
| State::Uploading
|
||||
| State::PausedUP
|
||||
| State::QueuedUP
|
||||
| State::StalledUP
|
||||
| State::CheckingUP => DownloadSimpleState::Completed,
|
||||
State::Error | State::MissingFiles => DownloadSimpleState::Error,
|
||||
State::Unknown => DownloadSimpleState::Unknown,
|
||||
State::PausedDL => DownloadSimpleState::Paused,
|
||||
State::Allocating
|
||||
| State::Moving
|
||||
| State::MetaDL
|
||||
| State::ForcedDL
|
||||
| State::CheckingResumeData
|
||||
| State::QueuedDL
|
||||
| State::Downloading
|
||||
| State::StalledDL
|
||||
| State::CheckingDL => DownloadSimpleState::Active,
|
||||
}
|
||||
} else {
|
||||
DownloadSimpleState::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TorrentStateTrait for QBittorrentState {}
|
||||
|
||||
@ -129,8 +156,8 @@ impl DownloadTaskTrait for QBittorrentTask {
|
||||
}
|
||||
|
||||
impl TorrentTaskTrait for QBittorrentTask {
|
||||
fn hash_info(&self) -> &str {
|
||||
&self.hash_info
|
||||
fn hash_info(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(&self.hash_info)
|
||||
}
|
||||
|
||||
fn tags(&self) -> impl Iterator<Item = Cow<'_, str>> {
|
||||
@ -177,6 +204,7 @@ impl TorrentCreationTrait for QBittorrentCreation {
|
||||
|
||||
pub type QBittorrentHashSelector = DownloadIdSelector<QBittorrentTask>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QBittorrentComplexSelector {
|
||||
pub query: GetTorrentListArg,
|
||||
}
|
||||
@ -197,6 +225,7 @@ impl DownloadSelectorTrait for QBittorrentComplexSelector {
|
||||
type Task = QBittorrentTask;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum QBittorrentSelector {
|
||||
Hash(QBittorrentHashSelector),
|
||||
Complex(QBittorrentComplexSelector),
|
||||
|
@ -1 +1,278 @@
|
||||
pub struct RqbitDownloaderCreation {}
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use librqbit::{
|
||||
AddTorrent, AddTorrentOptions, ManagedTorrent, Session, SessionOptions, api::TorrentIdOrHash,
|
||||
};
|
||||
use librqbit_core::Id20;
|
||||
use snafu::ResultExt;
|
||||
use tracing::instrument;
|
||||
use util::errors::AnyhowResultExt;
|
||||
|
||||
use super::task::{RqbitCreation, RqbitHash, RqbitSelector, RqbitState, RqbitTask};
|
||||
use crate::{
|
||||
DownloaderError,
|
||||
bittorrent::{
|
||||
downloader::TorrentDownloaderTrait,
|
||||
source::{HashTorrentSource, HashTorrentSourceTrait},
|
||||
},
|
||||
core::{DownloadIdSelector, DownloaderTrait},
|
||||
errors::RqbitSnafu,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RqbitDownloaderCreation {
|
||||
pub save_path: String,
|
||||
pub subscriber_id: i32,
|
||||
pub downloader_id: i32,
|
||||
}
|
||||
|
||||
impl RqbitDownloaderCreation {}
|
||||
|
||||
pub struct RqbitDownloader {
|
||||
pub save_path: String,
|
||||
pub subscriber_id: i32,
|
||||
pub downloader_id: i32,
|
||||
pub session: Arc<Session>,
|
||||
}
|
||||
|
||||
impl RqbitDownloader {
|
||||
#[instrument(level = "debug")]
|
||||
pub async fn from_creation(
|
||||
creation: RqbitDownloaderCreation,
|
||||
) -> Result<Arc<Self>, DownloaderError> {
|
||||
let session_opt = SessionOptions {
|
||||
..Default::default()
|
||||
};
|
||||
let session = Session::new_with_opts(creation.save_path.clone().into(), session_opt)
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
Ok(Arc::new(Self {
|
||||
session,
|
||||
save_path: creation.save_path,
|
||||
subscriber_id: creation.subscriber_id,
|
||||
downloader_id: creation.downloader_id,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn add_torrent(
|
||||
&self,
|
||||
source: HashTorrentSource,
|
||||
opt: Option<AddTorrentOptions>,
|
||||
) -> Result<RqbitHash, DownloaderError> {
|
||||
let hash = Id20::from_str(&source.hash_info() as &str)
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
let source = match source {
|
||||
HashTorrentSource::TorrentFile(file) => AddTorrent::TorrentFileBytes(file.payload),
|
||||
HashTorrentSource::MagnetUrl(magnet) => AddTorrent::Url(magnet.url.into()),
|
||||
};
|
||||
let response = self
|
||||
.session
|
||||
.add_torrent(source, opt)
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
let handle = response
|
||||
.into_handle()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to get handle of add torrent task"))
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
handle
|
||||
.wait_until_initialized()
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
fn query_torrent_impl(&self, hash: RqbitHash) -> Result<Arc<ManagedTorrent>, DownloaderError> {
|
||||
let torrent = self
|
||||
.session
|
||||
.get(TorrentIdOrHash::Hash(hash))
|
||||
.ok_or_else(|| anyhow::anyhow!("could not find torrent by hash {}", hash.as_string()))
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
Ok(torrent)
|
||||
}
|
||||
|
||||
pub fn query_torrent(&self, hash: RqbitHash) -> Result<RqbitTask, DownloaderError> {
|
||||
let torrent = self.query_torrent_impl(hash)?;
|
||||
|
||||
let task = RqbitTask::from_query(torrent)?;
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
pub async fn pause_torrent(&self, hash: RqbitHash) -> Result<(), DownloaderError> {
|
||||
let t = self.query_torrent_impl(hash)?;
|
||||
self.session
|
||||
.pause(&t)
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resume_torrent(&self, hash: RqbitHash) -> Result<(), DownloaderError> {
|
||||
let t = self.query_torrent_impl(hash)?;
|
||||
self.session
|
||||
.unpause(&t)
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_torrent(&self, hash: RqbitHash) -> Result<(), DownloaderError> {
|
||||
self.session
|
||||
.delete(TorrentIdOrHash::Hash(hash), true)
|
||||
.await
|
||||
.to_dyn_boxed()
|
||||
.context(RqbitSnafu {})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DownloaderTrait for RqbitDownloader {
|
||||
type State = RqbitState;
|
||||
type Id = RqbitHash;
|
||||
type Task = RqbitTask;
|
||||
type Creation = RqbitCreation;
|
||||
type Selector = RqbitSelector;
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn add_downloads(
|
||||
&self,
|
||||
creation: RqbitCreation,
|
||||
) -> Result<Vec<<Self as DownloaderTrait>::Id>, DownloaderError> {
|
||||
let mut sources = creation.sources;
|
||||
if sources.len() == 1 {
|
||||
let hash = self
|
||||
.add_torrent(
|
||||
sources.pop().unwrap(),
|
||||
Some(AddTorrentOptions {
|
||||
paused: false,
|
||||
output_folder: Some(self.save_path.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(vec![hash])
|
||||
} else {
|
||||
let tasks = sources
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
self.add_torrent(
|
||||
s,
|
||||
Some(AddTorrentOptions {
|
||||
paused: false,
|
||||
output_folder: Some(self.save_path.clone()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let results = futures::future::try_join_all(tasks).await?;
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
async fn pause_downloads(
|
||||
&self,
|
||||
selector: <Self as DownloaderTrait>::Selector,
|
||||
) -> Result<impl IntoIterator<Item = Self::Id>, DownloaderError> {
|
||||
<Self as TorrentDownloaderTrait>::pause_downloads(self, selector).await
|
||||
}
|
||||
|
||||
async fn resume_downloads(
|
||||
&self,
|
||||
selector: <Self as DownloaderTrait>::Selector,
|
||||
) -> Result<impl IntoIterator<Item = Self::Id>, DownloaderError> {
|
||||
<Self as TorrentDownloaderTrait>::resume_downloads(self, selector).await
|
||||
}
|
||||
|
||||
async fn remove_downloads(
|
||||
&self,
|
||||
selector: <Self as DownloaderTrait>::Selector,
|
||||
) -> Result<impl IntoIterator<Item = Self::Id>, DownloaderError> {
|
||||
<Self as TorrentDownloaderTrait>::remove_downloads(self, selector).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn query_downloads(
|
||||
&self,
|
||||
selector: RqbitSelector,
|
||||
) -> Result<Vec<<Self as DownloaderTrait>::Task>, DownloaderError> {
|
||||
let hashes = selector.into_iter();
|
||||
|
||||
let tasks = hashes
|
||||
.map(|h| self.query_torrent(h))
|
||||
.collect::<Result<Vec<_>, DownloaderError>>()?;
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TorrentDownloaderTrait for RqbitDownloader {
|
||||
type IdSelector = DownloadIdSelector<Self::Task>;
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn pause_torrents(
|
||||
&self,
|
||||
selector: Self::IdSelector,
|
||||
) -> Result<Self::IdSelector, DownloaderError> {
|
||||
let mut hashes: Vec<_> = selector.clone();
|
||||
|
||||
if hashes.len() == 1 {
|
||||
self.pause_torrent(hashes.pop().unwrap()).await?;
|
||||
} else {
|
||||
futures::future::try_join_all(hashes.into_iter().map(|h| self.pause_torrent(h)))
|
||||
.await?;
|
||||
}
|
||||
Ok(selector)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn resume_torrents(
|
||||
&self,
|
||||
selector: Self::IdSelector,
|
||||
) -> Result<Self::IdSelector, DownloaderError> {
|
||||
let mut hashes: Vec<_> = selector.clone();
|
||||
|
||||
if hashes.len() == 1 {
|
||||
self.resume_torrent(hashes.pop().unwrap()).await?;
|
||||
} else {
|
||||
futures::future::try_join_all(hashes.into_iter().map(|h| self.resume_torrent(h)))
|
||||
.await?;
|
||||
}
|
||||
Ok(selector)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn remove_torrents(
|
||||
&self,
|
||||
selector: Self::IdSelector,
|
||||
) -> Result<Self::IdSelector, DownloaderError> {
|
||||
let mut hashes: Vec<_> = selector.clone();
|
||||
|
||||
if hashes.len() == 1 {
|
||||
self.delete_torrent(hashes.pop().unwrap()).await?;
|
||||
} else {
|
||||
futures::future::try_join_all(hashes.into_iter().map(|h| self.delete_torrent(h)))
|
||||
.await?;
|
||||
}
|
||||
Ok(selector)
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +1,84 @@
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
use std::{borrow::Cow, fmt::Debug, sync::Arc, time::Duration};
|
||||
|
||||
use itertools::Itertools;
|
||||
use qbit_rs::model::{
|
||||
GetTorrentListArg, State, Torrent as QbitTorrent, TorrentContent as QbitTorrentContent,
|
||||
};
|
||||
use librqbit::{ManagedTorrent, ManagedTorrentState, TorrentStats, TorrentStatsState};
|
||||
use librqbit_core::Id20;
|
||||
use quirks_path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
DownloaderError,
|
||||
bittorrent::{
|
||||
source::HashTorrentSource,
|
||||
task::{SimpleTorrentHash, TorrentCreationTrait, TorrentStateTrait, TorrentTaskTrait},
|
||||
task::{TorrentCreationTrait, TorrentHashTrait, TorrentStateTrait, TorrentTaskTrait},
|
||||
},
|
||||
core::{
|
||||
DownloadCreationTrait, DownloadIdSelector, DownloadSelectorTrait, DownloadStateTrait,
|
||||
DownloadTaskTrait,
|
||||
DownloadCreationTrait, DownloadIdSelector, DownloadIdTrait, DownloadSimpleState,
|
||||
DownloadStateTrait, DownloadTaskTrait,
|
||||
},
|
||||
};
|
||||
|
||||
pub type RqbitHash = SimpleTorrentHash;
|
||||
pub type RqbitHash = Id20;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RqbitState(Option<State>);
|
||||
impl DownloadIdTrait for RqbitHash {}
|
||||
|
||||
impl DownloadStateTrait for RqbitState {}
|
||||
impl TorrentHashTrait for RqbitHash {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RqbitState(Arc<TorrentStats>);
|
||||
|
||||
impl DownloadStateTrait for RqbitState {
|
||||
fn to_download_state(&self) -> DownloadSimpleState {
|
||||
match self.0.state {
|
||||
TorrentStatsState::Error => DownloadSimpleState::Error,
|
||||
TorrentStatsState::Paused => DownloadSimpleState::Paused,
|
||||
TorrentStatsState::Live => {
|
||||
if self.0.finished {
|
||||
DownloadSimpleState::Completed
|
||||
} else {
|
||||
DownloadSimpleState::Active
|
||||
}
|
||||
}
|
||||
TorrentStatsState::Initializing => DownloadSimpleState::Active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TorrentStateTrait for RqbitState {}
|
||||
|
||||
impl From<Option<State>> for RqbitState {
|
||||
fn from(value: Option<State>) -> Self {
|
||||
impl From<Arc<TorrentStats>> for RqbitState {
|
||||
fn from(value: Arc<TorrentStats>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RqbitTask {
|
||||
pub hash_info: RqbitHash,
|
||||
pub torrent: QbitTorrent,
|
||||
pub contents: Vec<QbitTorrentContent>,
|
||||
pub torrent: Arc<ManagedTorrent>,
|
||||
pub state: RqbitState,
|
||||
pub stats: Arc<TorrentStats>,
|
||||
}
|
||||
|
||||
impl RqbitTask {
|
||||
pub fn from_query(
|
||||
torrent: QbitTorrent,
|
||||
contents: Vec<QbitTorrentContent>,
|
||||
) -> Result<Self, DownloaderError> {
|
||||
let hash = torrent
|
||||
.hash
|
||||
.clone()
|
||||
.ok_or_else(|| DownloaderError::TorrentMetaError {
|
||||
message: "missing hash".to_string(),
|
||||
source: None.into(),
|
||||
})?;
|
||||
let state = RqbitState::from(torrent.state.clone());
|
||||
pub fn from_query(torrent: Arc<ManagedTorrent>) -> Result<Self, DownloaderError> {
|
||||
let hash = torrent.info_hash();
|
||||
let stats = Arc::new(torrent.stats());
|
||||
Ok(Self {
|
||||
hash_info: hash,
|
||||
contents,
|
||||
state,
|
||||
state: stats.clone().into(),
|
||||
stats,
|
||||
torrent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for RqbitTask {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RqbitTask")
|
||||
.field("hash_info", &self.hash_info)
|
||||
.field("state", &self.id())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadTaskTrait for RqbitTask {
|
||||
type State = RqbitState;
|
||||
type Id = RqbitHash;
|
||||
@ -77,14 +93,26 @@ impl DownloadTaskTrait for RqbitTask {
|
||||
|
||||
fn name(&self) -> Cow<'_, str> {
|
||||
self.torrent
|
||||
.name
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.metadata
|
||||
.load_full()
|
||||
.and_then(|m| m.name.to_owned())
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| DownloadTaskTrait::name(self))
|
||||
}
|
||||
|
||||
fn speed(&self) -> Option<u64> {
|
||||
self.torrent.dlspeed.and_then(|s| u64::try_from(s).ok())
|
||||
self.stats
|
||||
.live
|
||||
.as_ref()
|
||||
.map(|s| s.download_speed.mbps)
|
||||
.and_then(|u| {
|
||||
let v = u * 1024f64 * 1024f64;
|
||||
if v.is_finite() && v > 0.0 && v < u64::MAX as f64 {
|
||||
Some(v as u64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn state(&self) -> &Self::State {
|
||||
@ -92,54 +120,41 @@ impl DownloadTaskTrait for RqbitTask {
|
||||
}
|
||||
|
||||
fn dl_bytes(&self) -> Option<u64> {
|
||||
self.torrent.downloaded.and_then(|v| u64::try_from(v).ok())
|
||||
Some(self.stats.progress_bytes)
|
||||
}
|
||||
|
||||
fn total_bytes(&self) -> Option<u64> {
|
||||
self.torrent.size.and_then(|v| u64::try_from(v).ok())
|
||||
}
|
||||
|
||||
fn left_bytes(&self) -> Option<u64> {
|
||||
self.torrent.amount_left.and_then(|v| u64::try_from(v).ok())
|
||||
Some(self.stats.total_bytes)
|
||||
}
|
||||
|
||||
fn et(&self) -> Option<Duration> {
|
||||
self.torrent
|
||||
.time_active
|
||||
.and_then(|v| u64::try_from(v).ok())
|
||||
.map(Duration::from_secs)
|
||||
self.torrent.with_state(|l| match l {
|
||||
ManagedTorrentState::Live(l) => Some(Duration::from_millis(
|
||||
l.stats_snapshot().total_piece_download_ms,
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn eta(&self) -> Option<Duration> {
|
||||
self.torrent
|
||||
.eta
|
||||
.and_then(|v| u64::try_from(v).ok())
|
||||
.map(Duration::from_secs)
|
||||
}
|
||||
|
||||
fn progress(&self) -> Option<f32> {
|
||||
self.torrent.progress.as_ref().map(|s| *s as f32)
|
||||
self.torrent.with_state(|l| match l {
|
||||
ManagedTorrentState::Live(l) => l.down_speed_estimator().time_remaining(),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TorrentTaskTrait for RqbitTask {
|
||||
fn hash_info(&self) -> &str {
|
||||
&self.hash_info
|
||||
fn hash_info(&self) -> Cow<'_, str> {
|
||||
Cow::Owned(self.hash_info.as_string())
|
||||
}
|
||||
|
||||
fn tags(&self) -> impl Iterator<Item = Cow<'_, str>> {
|
||||
self.torrent
|
||||
.tags
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(Cow::Borrowed)
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
fn category(&self) -> Option<Cow<'_, str>> {
|
||||
self.torrent.category.as_deref().map(Cow::Borrowed)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,45 +186,4 @@ impl TorrentCreationTrait for RqbitCreation {
|
||||
|
||||
pub type RqbitHashSelector = DownloadIdSelector<RqbitTask>;
|
||||
|
||||
pub struct RqbitComplexSelector {
|
||||
pub query: GetTorrentListArg,
|
||||
}
|
||||
|
||||
impl From<RqbitHashSelector> for RqbitComplexSelector {
|
||||
fn from(value: RqbitHashSelector) -> Self {
|
||||
Self {
|
||||
query: GetTorrentListArg {
|
||||
hashes: Some(value.ids.join("|")),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadSelectorTrait for RqbitComplexSelector {
|
||||
type Id = RqbitHash;
|
||||
type Task = RqbitTask;
|
||||
}
|
||||
|
||||
pub enum RqbitSelector {
|
||||
Hash(RqbitHashSelector),
|
||||
Complex(RqbitComplexSelector),
|
||||
}
|
||||
|
||||
impl DownloadSelectorTrait for RqbitSelector {
|
||||
type Id = RqbitHash;
|
||||
type Task = RqbitTask;
|
||||
|
||||
fn try_into_ids_only(self) -> Result<Vec<Self::Id>, Self> {
|
||||
match self {
|
||||
RqbitSelector::Complex(c) => c.try_into_ids_only().map_err(RqbitSelector::Complex),
|
||||
RqbitSelector::Hash(h) => {
|
||||
let result = h
|
||||
.try_into_ids_only()
|
||||
.unwrap_or_else(|_| unreachable!("hash selector must contains hash"))
|
||||
.into_iter();
|
||||
Ok(result.collect_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type RqbitSelector = RqbitHashSelector;
|
||||
|
Loading…
Reference in New Issue
Block a user