Move torrents to completed after finish

This commit is contained in:
Alexander
2026-07-24 16:47:42 +02:00
parent 9c2499a4a3
commit 3f3a6d354a
10 changed files with 1588 additions and 549 deletions
File diff suppressed because it is too large Load Diff
@@ -10,15 +10,24 @@ impl serde::Serialize for AddRequest {
if !self.magnet.is_empty() {
len += 1;
}
if !self.output_dir.is_empty() {
if !self.download_folder.is_empty() {
len += 1;
}
if !self.move_after_completion_folder.is_empty() {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("torrent.AddRequest", len)?;
if !self.magnet.is_empty() {
struct_ser.serialize_field("magnet", &self.magnet)?;
}
if !self.output_dir.is_empty() {
struct_ser.serialize_field("outputDir", &self.output_dir)?;
if !self.download_folder.is_empty() {
struct_ser.serialize_field("downloadFolder", &self.download_folder)?;
}
if !self.move_after_completion_folder.is_empty() {
struct_ser.serialize_field(
"moveAfterCompletionFolder",
&self.move_after_completion_folder,
)?;
}
struct_ser.end()
}
@@ -29,12 +38,19 @@ impl<'de> serde::Deserialize<'de> for AddRequest {
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &["magnet", "output_dir", "outputDir"];
const FIELDS: &[&str] = &[
"magnet",
"download_folder",
"downloadFolder",
"move_after_completion_folder",
"moveAfterCompletionFolder",
];
#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Magnet,
OutputDir,
DownloadFolder,
MoveAfterCompletionFolder,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
@@ -60,7 +76,12 @@ impl<'de> serde::Deserialize<'de> for AddRequest {
{
match value {
"magnet" => Ok(GeneratedField::Magnet),
"outputDir" | "output_dir" => Ok(GeneratedField::OutputDir),
"downloadFolder" | "download_folder" => {
Ok(GeneratedField::DownloadFolder)
}
"moveAfterCompletionFolder" | "move_after_completion_folder" => {
Ok(GeneratedField::MoveAfterCompletionFolder)
}
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
}
}
@@ -81,7 +102,8 @@ impl<'de> serde::Deserialize<'de> for AddRequest {
V: serde::de::MapAccess<'de>,
{
let mut magnet__ = None;
let mut output_dir__ = None;
let mut download_folder__ = None;
let mut move_after_completion_folder__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Magnet => {
@@ -90,17 +112,27 @@ impl<'de> serde::Deserialize<'de> for AddRequest {
}
magnet__ = Some(map_.next_value()?);
}
GeneratedField::OutputDir => {
if output_dir__.is_some() {
return Err(serde::de::Error::duplicate_field("outputDir"));
GeneratedField::DownloadFolder => {
if download_folder__.is_some() {
return Err(serde::de::Error::duplicate_field("downloadFolder"));
}
output_dir__ = Some(map_.next_value()?);
download_folder__ = Some(map_.next_value()?);
}
GeneratedField::MoveAfterCompletionFolder => {
if move_after_completion_folder__.is_some() {
return Err(serde::de::Error::duplicate_field(
"moveAfterCompletionFolder",
));
}
move_after_completion_folder__ = Some(map_.next_value()?);
}
}
}
Ok(AddRequest {
magnet: magnet__.unwrap_or_default(),
output_dir: output_dir__.unwrap_or_default(),
download_folder: download_folder__.unwrap_or_default(),
move_after_completion_folder: move_after_completion_folder__
.unwrap_or_default(),
})
}
}
@@ -291,6 +323,260 @@ impl<'de> serde::Deserialize<'de> for AddedInfo {
deserializer.deserialize_struct("torrent.AddedInfo", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for FileState {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let variant = match self {
Self::Unspecified => "FILE_STATE_UNSPECIFIED",
Self::InProgress => "IN_PROGRESS",
Self::Ready => "READY",
};
serializer.serialize_str(variant)
}
}
impl<'de> serde::Deserialize<'de> for FileState {
#[allow(deprecated)]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &["FILE_STATE_UNSPECIFIED", "IN_PROGRESS", "READY"];
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = FileState;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "expected one of: {:?}", &FIELDS)
}
fn visit_i64<E>(self, v: i64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
i32::try_from(v)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self)
})
}
fn visit_u64<E>(self, v: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
i32::try_from(v)
.ok()
.and_then(|x| x.try_into().ok())
.ok_or_else(|| {
serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self)
})
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
"FILE_STATE_UNSPECIFIED" => Ok(FileState::Unspecified),
"IN_PROGRESS" => Ok(FileState::InProgress),
"READY" => Ok(FileState::Ready),
_ => Err(serde::de::Error::unknown_variant(value, FIELDS)),
}
}
}
deserializer.deserialize_any(GeneratedVisitor)
}
}
impl serde::Serialize for FilesRequest {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut len = 0;
if !self.id.is_empty() {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("torrent.FilesRequest", len)?;
if !self.id.is_empty() {
struct_ser.serialize_field("id", &self.id)?;
}
struct_ser.end()
}
}
impl<'de> serde::Deserialize<'de> for FilesRequest {
#[allow(deprecated)]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &["id"];
#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Id,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
where
D: serde::Deserializer<'de>,
{
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = GeneratedField;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(formatter, "expected one of: {:?}", &FIELDS)
}
#[allow(unused_variables)]
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
where
E: serde::de::Error,
{
match value {
"id" => Ok(GeneratedField::Id),
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(GeneratedVisitor)
}
}
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = FilesRequest;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("struct torrent.FilesRequest")
}
fn visit_map<V>(self, mut map_: V) -> std::result::Result<FilesRequest, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut id__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Id => {
if id__.is_some() {
return Err(serde::de::Error::duplicate_field("id"));
}
id__ = Some(map_.next_value()?);
}
}
}
Ok(FilesRequest {
id: id__.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct("torrent.FilesRequest", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for FilesResponse {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut len = 0;
if !self.files.is_empty() {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("torrent.FilesResponse", len)?;
if !self.files.is_empty() {
struct_ser.serialize_field("files", &self.files)?;
}
struct_ser.end()
}
}
impl<'de> serde::Deserialize<'de> for FilesResponse {
#[allow(deprecated)]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &["files"];
#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Files,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
where
D: serde::Deserializer<'de>,
{
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = GeneratedField;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(formatter, "expected one of: {:?}", &FIELDS)
}
#[allow(unused_variables)]
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
where
E: serde::de::Error,
{
match value {
"files" => Ok(GeneratedField::Files),
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(GeneratedVisitor)
}
}
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = FilesResponse;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("struct torrent.FilesResponse")
}
fn visit_map<V>(self, mut map_: V) -> std::result::Result<FilesResponse, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut files__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Files => {
if files__.is_some() {
return Err(serde::de::Error::duplicate_field("files"));
}
files__ = Some(map_.next_value()?);
}
}
}
Ok(FilesResponse {
files: files__.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct("torrent.FilesResponse", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for ListRequest {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
@@ -1876,6 +2162,170 @@ impl<'de> serde::Deserialize<'de> for StatusRequest {
deserializer.deserialize_struct("torrent.StatusRequest", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for TorrentFile {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut len = 0;
if !self.name.is_empty() {
len += 1;
}
if self.length != 0 {
len += 1;
}
if self.downloaded_bytes != 0 {
len += 1;
}
if self.state != 0 {
len += 1;
}
let mut struct_ser = serializer.serialize_struct("torrent.TorrentFile", len)?;
if !self.name.is_empty() {
struct_ser.serialize_field("name", &self.name)?;
}
if self.length != 0 {
#[allow(clippy::needless_borrow)]
#[allow(clippy::needless_borrows_for_generic_args)]
struct_ser.serialize_field("length", ToString::to_string(&self.length).as_str())?;
}
if self.downloaded_bytes != 0 {
#[allow(clippy::needless_borrow)]
#[allow(clippy::needless_borrows_for_generic_args)]
struct_ser.serialize_field(
"downloadedBytes",
ToString::to_string(&self.downloaded_bytes).as_str(),
)?;
}
if self.state != 0 {
let v = FileState::try_from(self.state).map_err(|_| {
serde::ser::Error::custom(format!("Invalid variant {}", self.state))
})?;
struct_ser.serialize_field("state", &v)?;
}
struct_ser.end()
}
}
impl<'de> serde::Deserialize<'de> for TorrentFile {
#[allow(deprecated)]
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
const FIELDS: &[&str] = &[
"name",
"length",
"downloaded_bytes",
"downloadedBytes",
"state",
];
#[allow(clippy::enum_variant_names)]
enum GeneratedField {
Name,
Length,
DownloadedBytes,
State,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) -> std::result::Result<GeneratedField, D::Error>
where
D: serde::Deserializer<'de>,
{
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = GeneratedField;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
write!(formatter, "expected one of: {:?}", &FIELDS)
}
#[allow(unused_variables)]
fn visit_str<E>(self, value: &str) -> std::result::Result<GeneratedField, E>
where
E: serde::de::Error,
{
match value {
"name" => Ok(GeneratedField::Name),
"length" => Ok(GeneratedField::Length),
"downloadedBytes" | "downloaded_bytes" => {
Ok(GeneratedField::DownloadedBytes)
}
"state" => Ok(GeneratedField::State),
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(GeneratedVisitor)
}
}
struct GeneratedVisitor;
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
type Value = TorrentFile;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("struct torrent.TorrentFile")
}
fn visit_map<V>(self, mut map_: V) -> std::result::Result<TorrentFile, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut name__ = None;
let mut length__ = None;
let mut downloaded_bytes__ = None;
let mut state__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::Name => {
if name__.is_some() {
return Err(serde::de::Error::duplicate_field("name"));
}
name__ = Some(map_.next_value()?);
}
GeneratedField::Length => {
if length__.is_some() {
return Err(serde::de::Error::duplicate_field("length"));
}
length__ = Some(
map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?
.0,
);
}
GeneratedField::DownloadedBytes => {
if downloaded_bytes__.is_some() {
return Err(serde::de::Error::duplicate_field("downloadedBytes"));
}
downloaded_bytes__ = Some(
map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?
.0,
);
}
GeneratedField::State => {
if state__.is_some() {
return Err(serde::de::Error::duplicate_field("state"));
}
state__ = Some(map_.next_value::<FileState>()? as i32);
}
}
}
Ok(TorrentFile {
name: name__.unwrap_or_default(),
length: length__.unwrap_or_default(),
downloaded_bytes: downloaded_bytes__.unwrap_or_default(),
state: state__.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct("torrent.TorrentFile", FIELDS, GeneratedVisitor)
}
}
impl serde::Serialize for TorrentStatus {
#[allow(deprecated)]
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
@@ -190,6 +190,20 @@ pub mod torrents_client {
.insert(GrpcMethod::new("torrent.Torrents", "Notifications"));
self.inner.server_streaming(req, path, codec).await
}
pub async fn files(
&mut self,
request: impl tonic::IntoRequest<super::FilesRequest>,
) -> std::result::Result<tonic::Response<super::FilesResponse>, tonic::Status> {
self.inner.ready().await.map_err(|e| {
tonic::Status::unknown(format!("Service was not ready: {}", e.into()))
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/torrent.Torrents/Files");
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("torrent.Torrents", "Files"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
@@ -238,6 +252,10 @@ pub mod torrents_server {
&self,
request: tonic::Request<super::NotificationsRequest>,
) -> std::result::Result<tonic::Response<Self::NotificationsStream>, tonic::Status>;
async fn files(
&self,
request: tonic::Request<super::FilesRequest>,
) -> std::result::Result<tonic::Response<super::FilesResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct TorrentsServer<T> {
@@ -578,6 +596,43 @@ pub mod torrents_server {
};
Box::pin(fut)
}
"/torrent.Torrents/Files" => {
#[allow(non_camel_case_types)]
struct FilesSvc<T: Torrents>(pub Arc<T>);
impl<T: Torrents> tonic::server::UnaryService<super::FilesRequest> for FilesSvc<T> {
type Response = super::FilesResponse;
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
fn call(
&mut self,
request: tonic::Request<super::FilesRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move { <T as Torrents>::files(&inner, request).await };
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = FilesSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => Box::pin(async move {
let mut response = http::Response::new(tonic::body::Body::default());
let headers = response.headers_mut();
+4 -3
View File
@@ -4,9 +4,10 @@ pub mod torrent {
pub use torrent::notification as notification_detail;
pub use torrent::{
AddRequest, AddResponse, AddedInfo, ListRequest, ListResponse, Notification, NotificationKind,
NotificationsRequest, PauseRequest, PauseResponse, ProgressMark, RemoveRequest, RemoveResponse,
RemovedInfo, ResumeRequest, ResumeResponse, State, StateChanged, StatusRequest, TorrentStatus,
AddRequest, AddResponse, AddedInfo, FileState, FilesRequest, FilesResponse, ListRequest,
ListResponse, Notification, NotificationKind, NotificationsRequest, PauseRequest,
PauseResponse, ProgressMark, RemoveRequest, RemoveResponse, RemovedInfo, ResumeRequest,
ResumeResponse, State, StateChanged, StatusRequest, TorrentFile, TorrentStatus,
torrents_client::TorrentsClient,
torrents_server::{Torrents, TorrentsServer},
};