Add search for torrents
This commit is contained in:
@@ -4,3 +4,6 @@
|
||||
[submodule "tora"]
|
||||
path = tora
|
||||
url = /home/fujin/Code/agregators/tora
|
||||
[submodule "metadata-agregator"]
|
||||
path = metadata-agregator
|
||||
url = ssh://git@gitea.susano-homelab.duckdns.org:2222/fujin/metadata-agregator.git
|
||||
|
||||
Generated
+1126
-6
File diff suppressed because it is too large
Load Diff
@@ -25,5 +25,7 @@ tokio = { version = "1.52.3", features = [ "macros", "rt-multi-thread" ] }
|
||||
tonic-reflection = "0.14.6"
|
||||
tonic-health = "0.14.6"
|
||||
toml = "1.1.2"
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["query", "rustls"] }
|
||||
quick-xml = { version = "0.41.0", features = ["serialize"] }
|
||||
|
||||
[workspace]
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
version: v2
|
||||
inputs:
|
||||
- directory: proto
|
||||
- directory: ../metadata-agregator/proto
|
||||
- directory: ../musicfs/proto
|
||||
- directory: ../tora/proto
|
||||
- directory: ./metadata-agregator/proto
|
||||
- directory: ./musicfs/proto
|
||||
- directory: ./tora/proto
|
||||
plugins:
|
||||
- local: protoc-gen-prost
|
||||
#path: ../../target/debug/protoc-gen-prost
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
# Rootless container runtime. No daemon needed, unlike Docker.
|
||||
packages = [ pkgs.podman ];
|
||||
|
||||
# Jackett indexer, running rootless via podman so it joins `devenv up`
|
||||
# alongside postgres/torad/musicfs. Foreground (no `-d`) so process-compose
|
||||
# supervises it and streams logs; `--rm` cleans up on `devenv up` shutdown.
|
||||
# UI: http://localhost:9117
|
||||
#
|
||||
# Note: podman (unlike docker) does NOT auto-create bind-mount source dirs,
|
||||
# so we `mkdir -p` them first or podman fails with "statfs: no such file".
|
||||
processes.jackett.exec =
|
||||
let
|
||||
jackettConfig = "${config.env.DEVENV_RUNTIME}/jackett-config";
|
||||
jackettDownloads = "${config.env.DEVENV_RUNTIME}/jackett-downloads";
|
||||
in
|
||||
''
|
||||
mkdir -p "${jackettConfig}" "${jackettDownloads}"
|
||||
${pkgs.podman}/bin/podman run --rm \
|
||||
--name devenv-jackett \
|
||||
-p 9117:9117 \
|
||||
-e PUID=1000 \
|
||||
-e PGID=100 \
|
||||
-e TZ=Europe/Warsaw \
|
||||
-e AUTO_UPDATE=true \
|
||||
-v "${jackettConfig}":/config \
|
||||
-v "${jackettDownloads}":/downloads \
|
||||
lscr.io/linuxserver/jackett:latest
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Secrets
|
||||
.env*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,11 @@
|
||||
info:
|
||||
name: Health
|
||||
type: grpc
|
||||
seq: 1
|
||||
|
||||
grpc:
|
||||
url: localhost:50051
|
||||
method: /health.Health/Check
|
||||
methodType: unary
|
||||
message: "{}"
|
||||
auth: inherit
|
||||
@@ -0,0 +1,24 @@
|
||||
info:
|
||||
name: Search
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: GET
|
||||
url: http://localhost:9117/api/v2.0/indexers/all/results/torznab/api?apikey=bjivq1zp3bfq7eaorhsswkfhvpc5issh&t=search&q=ДДТ
|
||||
params:
|
||||
- name: apikey
|
||||
value: bjivq1zp3bfq7eaorhsswkfhvpc5issh
|
||||
type: query
|
||||
- name: t
|
||||
value: search
|
||||
type: query
|
||||
- name: q
|
||||
value: ДДТ
|
||||
type: query
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: Jackett
|
||||
type: folder
|
||||
seq: 3
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
@@ -0,0 +1,14 @@
|
||||
info:
|
||||
name: Search
|
||||
type: grpc
|
||||
seq: 2
|
||||
|
||||
grpc:
|
||||
url: localhost:50051
|
||||
method: /torrent_manager.TorrentManager/Search
|
||||
methodType: unary
|
||||
message: |-
|
||||
{
|
||||
"name": "ДДТ"
|
||||
}
|
||||
auth: inherit
|
||||
@@ -0,0 +1,7 @@
|
||||
info:
|
||||
name: Torrent
|
||||
type: folder
|
||||
seq: 2
|
||||
|
||||
request:
|
||||
auth: inherit
|
||||
@@ -0,0 +1,25 @@
|
||||
opencollection: 1.0.0
|
||||
|
||||
info:
|
||||
name: Music Agregator
|
||||
config:
|
||||
proxy:
|
||||
inherit: true
|
||||
config:
|
||||
protocol: http
|
||||
hostname: ""
|
||||
port: ""
|
||||
auth:
|
||||
username: ""
|
||||
password: ""
|
||||
bypassProxy: ""
|
||||
bundled: false
|
||||
extensions:
|
||||
bruno:
|
||||
ignore:
|
||||
- node_modules
|
||||
- .git
|
||||
presets:
|
||||
request:
|
||||
type: grpc
|
||||
url: localhost:50051
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
imports = [ ./containers/jackett.nix ];
|
||||
|
||||
languages.rust.enable = true;
|
||||
|
||||
git-hooks.hooks = {
|
||||
|
||||
@@ -6,3 +6,6 @@ build: proto
|
||||
|
||||
run *FLAGS: proto
|
||||
cargo run -- {{FLAGS}}
|
||||
|
||||
up:
|
||||
devenv up
|
||||
|
||||
Submodule
+1
Submodule metadata-agregator added at 995397e495
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package torrent_manager;
|
||||
|
||||
service TorrentManager {
|
||||
rpc Search(SearchRequest) returns (SearchResponse);
|
||||
}
|
||||
|
||||
|
||||
message SearchRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message SearchResponse {
|
||||
repeated SearchItem item = 1;
|
||||
}
|
||||
|
||||
message SearchItem {
|
||||
string name = 1;
|
||||
string url = 2;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Declares the edition for `rustfmt` when it parses buffers via stdin
|
||||
# (e.g. emacs `rust-format-buffer` / DOOM `format-all`). rustfmt does not
|
||||
# consult Cargo.toml for stdin input, so without this it defaults to 2015
|
||||
# and rejects `async fn` with E0670. Must match edition in Cargo.toml.
|
||||
edition = "2024"
|
||||
@@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub service: ServiceConfig,
|
||||
pub indexer: IndexerConfig,
|
||||
pub torrent: TorrentConfig,
|
||||
}
|
||||
|
||||
@@ -11,6 +12,12 @@ pub struct ServiceConfig {
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IndexerConfig {
|
||||
pub address: String,
|
||||
pub api: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TorrentConfig {
|
||||
pub address: String,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
// @generated
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct SearchRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub name: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for SearchRequest {
|
||||
const NAME: &'static str = "SearchRequest";
|
||||
const PACKAGE: &'static str = "torrent_manager";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent_manager.SearchRequest".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent_manager.SearchRequest".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct SearchResponse {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub item: ::prost::alloc::vec::Vec<SearchItem>,
|
||||
}
|
||||
impl ::prost::Name for SearchResponse {
|
||||
const NAME: &'static str = "SearchResponse";
|
||||
const PACKAGE: &'static str = "torrent_manager";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent_manager.SearchResponse".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent_manager.SearchResponse".into()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct SearchItem {
|
||||
#[prost(string, tag = "1")]
|
||||
pub name: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub url: ::prost::alloc::string::String,
|
||||
}
|
||||
impl ::prost::Name for SearchItem {
|
||||
const NAME: &'static str = "SearchItem";
|
||||
const PACKAGE: &'static str = "torrent_manager";
|
||||
fn full_name() -> ::prost::alloc::string::String {
|
||||
"torrent_manager.SearchItem".into()
|
||||
}
|
||||
fn type_url() -> ::prost::alloc::string::String {
|
||||
"/torrent_manager.SearchItem".into()
|
||||
}
|
||||
}
|
||||
/// Encoded file descriptor set for the `torrent_manager` package
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] = &[
|
||||
0x0a, 0xc0, 0x05, 0x0a, 0x0c, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x12, 0x0f, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
|
||||
0x65, 0x72, 0x22, 0x23, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x41, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63,
|
||||
0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x69, 0x74, 0x65,
|
||||
0x6d, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x6f, 0x72, 0x72, 0x65, 0x6e,
|
||||
0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
|
||||
0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x32, 0x0a, 0x0a, 0x53, 0x65,
|
||||
0x61, 0x72, 0x63, 0x68, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
|
||||
0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x32, 0x5b,
|
||||
0x0a, 0x0e, 0x54, 0x6f, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72,
|
||||
0x12, 0x49, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1e, 0x2e, 0x74, 0x6f, 0x72,
|
||||
0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x74, 0x6f, 0x72,
|
||||
0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x61,
|
||||
0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4a, 0x9d, 0x03, 0x0a, 0x06,
|
||||
0x12, 0x04, 0x00, 0x00, 0x14, 0x01, 0x0a, 0x08, 0x0a, 0x01, 0x0c, 0x12, 0x03, 0x00, 0x00, 0x12,
|
||||
0x0a, 0x08, 0x0a, 0x01, 0x02, 0x12, 0x03, 0x02, 0x00, 0x18, 0x0a, 0x0a, 0x0a, 0x02, 0x06, 0x00,
|
||||
0x12, 0x04, 0x04, 0x00, 0x06, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x06, 0x00, 0x01, 0x12, 0x03, 0x04,
|
||||
0x08, 0x16, 0x0a, 0x0b, 0x0a, 0x04, 0x06, 0x00, 0x02, 0x00, 0x12, 0x03, 0x05, 0x02, 0x35, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x06, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x05, 0x06, 0x0c, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x06, 0x00, 0x02, 0x00, 0x02, 0x12, 0x03, 0x05, 0x0d, 0x1a, 0x0a, 0x0c, 0x0a, 0x05, 0x06,
|
||||
0x00, 0x02, 0x00, 0x03, 0x12, 0x03, 0x05, 0x25, 0x33, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x00, 0x12,
|
||||
0x04, 0x09, 0x00, 0x0b, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x00, 0x01, 0x12, 0x03, 0x09, 0x08,
|
||||
0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00, 0x02, 0x00, 0x05, 0x12, 0x03, 0x0a, 0x02, 0x08, 0x0a,
|
||||
0x0b, 0x0a, 0x04, 0x04, 0x00, 0x02, 0x00, 0x12, 0x03, 0x0a, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05,
|
||||
0x04, 0x00, 0x02, 0x00, 0x01, 0x12, 0x03, 0x0a, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x00,
|
||||
0x02, 0x00, 0x03, 0x12, 0x03, 0x0a, 0x10, 0x11, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x01, 0x12, 0x04,
|
||||
0x0d, 0x00, 0x0f, 0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x01, 0x01, 0x12, 0x03, 0x0d, 0x08, 0x16,
|
||||
0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x04, 0x12, 0x03, 0x0e, 0x02, 0x0a, 0x0a, 0x0b,
|
||||
0x0a, 0x04, 0x04, 0x01, 0x02, 0x00, 0x12, 0x03, 0x0e, 0x02, 0x1f, 0x0a, 0x0c, 0x0a, 0x05, 0x04,
|
||||
0x01, 0x02, 0x00, 0x06, 0x12, 0x03, 0x0e, 0x0b, 0x15, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02,
|
||||
0x00, 0x01, 0x12, 0x03, 0x0e, 0x16, 0x1a, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x01, 0x02, 0x00, 0x03,
|
||||
0x12, 0x03, 0x0e, 0x1d, 0x1e, 0x0a, 0x0a, 0x0a, 0x02, 0x04, 0x02, 0x12, 0x04, 0x11, 0x00, 0x14,
|
||||
0x01, 0x0a, 0x0a, 0x0a, 0x03, 0x04, 0x02, 0x01, 0x12, 0x03, 0x11, 0x08, 0x12, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x04, 0x02, 0x02, 0x00, 0x05, 0x12, 0x03, 0x12, 0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04,
|
||||
0x02, 0x02, 0x00, 0x12, 0x03, 0x12, 0x02, 0x12, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00,
|
||||
0x01, 0x12, 0x03, 0x12, 0x09, 0x0d, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x00, 0x03, 0x12,
|
||||
0x03, 0x12, 0x10, 0x11, 0x0a, 0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x01, 0x05, 0x12, 0x03, 0x13,
|
||||
0x02, 0x08, 0x0a, 0x0b, 0x0a, 0x04, 0x04, 0x02, 0x02, 0x01, 0x12, 0x03, 0x13, 0x02, 0x11, 0x0a,
|
||||
0x0c, 0x0a, 0x05, 0x04, 0x02, 0x02, 0x01, 0x01, 0x12, 0x03, 0x13, 0x09, 0x0c, 0x0a, 0x0c, 0x0a,
|
||||
0x05, 0x04, 0x02, 0x02, 0x01, 0x03, 0x12, 0x03, 0x13, 0x0f, 0x10, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
];
|
||||
include!("torrent_manager.serde.rs");
|
||||
include!("torrent_manager.tonic.rs");
|
||||
// @@protoc_insertion_point(module)
|
||||
@@ -0,0 +1,293 @@
|
||||
// @generated
|
||||
impl serde::Serialize for SearchItem {
|
||||
#[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.url.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent_manager.SearchItem", len)?;
|
||||
if !self.name.is_empty() {
|
||||
struct_ser.serialize_field("name", &self.name)?;
|
||||
}
|
||||
if !self.url.is_empty() {
|
||||
struct_ser.serialize_field("url", &self.url)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for SearchItem {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["name", "url"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Name,
|
||||
Url,
|
||||
}
|
||||
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),
|
||||
"url" => Ok(GeneratedField::Url),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = SearchItem;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent_manager.SearchItem")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<SearchItem, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut name__ = None;
|
||||
let mut url__ = 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::Url => {
|
||||
if url__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("url"));
|
||||
}
|
||||
url__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(SearchItem {
|
||||
name: name__.unwrap_or_default(),
|
||||
url: url__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent_manager.SearchItem", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for SearchRequest {
|
||||
#[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;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent_manager.SearchRequest", len)?;
|
||||
if !self.name.is_empty() {
|
||||
struct_ser.serialize_field("name", &self.name)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for SearchRequest {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["name"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Name,
|
||||
}
|
||||
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),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = SearchRequest;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent_manager.SearchRequest")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<SearchRequest, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut name__ = 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()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(SearchRequest {
|
||||
name: name__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent_manager.SearchRequest", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
impl serde::Serialize for SearchResponse {
|
||||
#[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.item.is_empty() {
|
||||
len += 1;
|
||||
}
|
||||
let mut struct_ser = serializer.serialize_struct("torrent_manager.SearchResponse", len)?;
|
||||
if !self.item.is_empty() {
|
||||
struct_ser.serialize_field("item", &self.item)?;
|
||||
}
|
||||
struct_ser.end()
|
||||
}
|
||||
}
|
||||
impl<'de> serde::Deserialize<'de> for SearchResponse {
|
||||
#[allow(deprecated)]
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
const FIELDS: &[&str] = &["item"];
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum GeneratedField {
|
||||
Item,
|
||||
}
|
||||
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 {
|
||||
"item" => Ok(GeneratedField::Item),
|
||||
_ => Err(serde::de::Error::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_identifier(GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
struct GeneratedVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for GeneratedVisitor {
|
||||
type Value = SearchResponse;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("struct torrent_manager.SearchResponse")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map_: V) -> std::result::Result<SearchResponse, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut item__ = None;
|
||||
while let Some(k) = map_.next_key()? {
|
||||
match k {
|
||||
GeneratedField::Item => {
|
||||
if item__.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("item"));
|
||||
}
|
||||
item__ = Some(map_.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(SearchResponse {
|
||||
item: item__.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct("torrent_manager.SearchResponse", FIELDS, GeneratedVisitor)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// @generated
|
||||
/// Generated client implementations.
|
||||
pub mod torrent_manager_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value
|
||||
)]
|
||||
use tonic::codegen::http::Uri;
|
||||
use tonic::codegen::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TorrentManagerClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl TorrentManagerClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> TorrentManagerClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> TorrentManagerClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<http::Request<tonic::body::Body>>>::Error:
|
||||
Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
TorrentManagerClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn search(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::SearchRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::SearchResponse>, 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_manager.TorrentManager/Search");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("torrent_manager.TorrentManager", "Search"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod torrent_manager_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with TorrentManagerServer.
|
||||
#[async_trait]
|
||||
pub trait TorrentManager: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn search(
|
||||
&self,
|
||||
request: tonic::Request<super::SearchRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct TorrentManagerServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> TorrentManagerServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(inner: T, interceptor: F) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for TorrentManagerServer<T>
|
||||
where
|
||||
T: TorrentManager,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/torrent_manager.TorrentManager/Search" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct SearchSvc<T: TorrentManager>(pub Arc<T>);
|
||||
impl<T: TorrentManager> tonic::server::UnaryService<super::SearchRequest> for SearchSvc<T> {
|
||||
type Response = super::SearchResponse;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::SearchRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut =
|
||||
async move { <T as TorrentManager>::search(&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 = SearchSvc(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();
|
||||
headers.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for TorrentManagerServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "torrent_manager.TorrentManager";
|
||||
impl<T> tonic::server::NamedService for TorrentManagerServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::IndexerConfig;
|
||||
|
||||
const TORZNAB_PATH: &str = "api/v2.0/indexers/all/results/torznab/api";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SearchResult {
|
||||
pub title: String,
|
||||
pub link: String,
|
||||
pub size: Option<u64>,
|
||||
pub seeders: Option<u32>,
|
||||
pub peers: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IndexerError {
|
||||
Request(reqwest::Error),
|
||||
Parse(quick_xml::de::DeError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IndexerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Request(err) => write!(f, "jackett request failed: {err}"),
|
||||
Self::Parse(err) => write!(f, "jackett response could not be parsed: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IndexerError {}
|
||||
|
||||
impl From<reqwest::Error> for IndexerError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
Self::Request(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<quick_xml::de::DeError> for IndexerError {
|
||||
fn from(err: quick_xml::de::DeError) -> Self {
|
||||
Self::Parse(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
pub trait Indexer: Send + Sync {
|
||||
async fn search(&self, query: &str) -> Result<Vec<SearchResult>, IndexerError>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Jackett {
|
||||
http: reqwest::Client,
|
||||
address: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl Jackett {
|
||||
pub fn new(config: &IndexerConfig) -> Self {
|
||||
Self {
|
||||
http: reqwest::Client::new(),
|
||||
address: config.address.trim_end_matches('/').to_string(),
|
||||
api_key: config.api.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Indexer for Jackett {
|
||||
async fn search(&self, query: &str) -> Result<Vec<SearchResult>, IndexerError> {
|
||||
let url = format!("{}/{TORZNAB_PATH}", self.address);
|
||||
|
||||
let body = self
|
||||
.http
|
||||
.get(url)
|
||||
.query(&[
|
||||
("t", "search"),
|
||||
("apikey", self.api_key.as_str()),
|
||||
("q", query),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
parse_results(&body)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_results(body: &str) -> Result<Vec<SearchResult>, IndexerError> {
|
||||
let rss: Rss = quick_xml::de::from_str(body)?;
|
||||
Ok(rss.channel.items.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Rss {
|
||||
channel: Channel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Channel {
|
||||
#[serde(rename = "item", default)]
|
||||
items: Vec<Item>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Item {
|
||||
title: String,
|
||||
link: String,
|
||||
#[serde(rename = "attr", default)]
|
||||
attrs: Vec<Attr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Attr {
|
||||
#[serde(rename = "@name")]
|
||||
name: String,
|
||||
#[serde(rename = "@value")]
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
fn attr(&self, name: &str) -> Option<&str> {
|
||||
self.attrs
|
||||
.iter()
|
||||
.find(|attr| attr.name == name)
|
||||
.map(|attr| attr.value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Item> for SearchResult {
|
||||
fn from(item: Item) -> Self {
|
||||
SearchResult {
|
||||
size: item.attr("size").and_then(|v| v.parse().ok()),
|
||||
seeders: item.attr("seeders").and_then(|v| v.parse().ok()),
|
||||
peers: item.attr("peers").and_then(|v| v.parse().ok()),
|
||||
title: item.title,
|
||||
link: item.link,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE_RESPONSE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:torznab="http://torznab.com/schemas/2015/feed">
|
||||
<channel>
|
||||
<title>Jackett</title>
|
||||
<item>
|
||||
<title>Some.Album.2020.FLAC</title>
|
||||
<guid>https://example.com/123</guid>
|
||||
<link>https://example.com/download/123.torrent</link>
|
||||
<pubDate>Mon, 01 Jan 2024 00:00:00 +0000</pubDate>
|
||||
<torznab:attr name="category" value="3040"/>
|
||||
<torznab:attr name="size" value="1073741824"/>
|
||||
<torznab:attr name="seeders" value="42"/>
|
||||
<torznab:attr name="peers" value="50"/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>"#;
|
||||
|
||||
#[test]
|
||||
fn parses_torznab_results() {
|
||||
let results = parse_results(SAMPLE_RESPONSE).expect("valid torznab response");
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![SearchResult {
|
||||
title: "Some.Album.2020.FLAC".to_string(),
|
||||
link: "https://example.com/download/123.torrent".to_string(),
|
||||
size: Some(1_073_741_824),
|
||||
seeders: Some(42),
|
||||
peers: Some(50),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_empty_channel() {
|
||||
let results = parse_results(
|
||||
r#"<rss xmlns:torznab="http://torznab.com/schemas/2015/feed"><channel></channel></rss>"#,
|
||||
)
|
||||
.expect("valid torznab response");
|
||||
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -1,7 +1,10 @@
|
||||
mod config;
|
||||
mod greeter;
|
||||
mod health;
|
||||
mod indexer;
|
||||
mod torrent;
|
||||
mod torrent_manager;
|
||||
|
||||
mod generated {
|
||||
pub mod hello {
|
||||
include!("generated/hello/hello.rs");
|
||||
@@ -11,21 +14,28 @@ mod generated {
|
||||
include!("generated/torrent/torrent.rs");
|
||||
}
|
||||
|
||||
pub mod torrent_manager {
|
||||
include!("generated/torrent_manager/torrent_manager.rs");
|
||||
}
|
||||
|
||||
pub mod health {
|
||||
include!("generated/health/health.rs");
|
||||
}
|
||||
}
|
||||
|
||||
use std::fs;
|
||||
use std::{fs, sync::Arc};
|
||||
|
||||
use generated::{
|
||||
health::health_server::HealthServer as AggregatorHealthServer,
|
||||
hello::greeter_server::GreeterServer, torrent::torrents_server::TorrentsServer,
|
||||
torrent_manager::torrent_manager_server::TorrentManagerServer,
|
||||
};
|
||||
use greeter::GreeterService;
|
||||
use health::HealthService;
|
||||
use indexer::Jackett;
|
||||
use tonic_health::pb::health_server::HealthServer;
|
||||
use torrent::TorrentsService;
|
||||
use torrent_manager::TorrentMananagerService;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -37,11 +47,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.register_encoded_file_descriptor_set(generated::hello::FILE_DESCRIPTOR_SET)
|
||||
.register_encoded_file_descriptor_set(generated::torrent::FILE_DESCRIPTOR_SET)
|
||||
.register_encoded_file_descriptor_set(generated::health::FILE_DESCRIPTOR_SET)
|
||||
.register_encoded_file_descriptor_set(generated::torrent_manager::FILE_DESCRIPTOR_SET)
|
||||
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
|
||||
.build_v1()?;
|
||||
|
||||
let torrents = TorrentsService::new(&config.torrent.address);
|
||||
let health = HealthService::new(torrents.channel());
|
||||
let indexer = Arc::new(Jackett::new(&config.indexer));
|
||||
let torrent_manager = TorrentMananagerService::new(torrents.clone(), indexer);
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(reflection)
|
||||
@@ -49,6 +62,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.add_service(AggregatorHealthServer::new(health))
|
||||
.add_service(GreeterServer::new(GreeterService::default()))
|
||||
.add_service(TorrentsServer::new(torrents))
|
||||
.add_service(TorrentManagerServer::new(torrent_manager))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use crate::generated::torrent::{torrents_client::TorrentsClient, torrents_server::Torrents, *};
|
||||
use tonic::transport::{Channel, Endpoint};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TorrentsService {
|
||||
channel: Channel,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
generated::{
|
||||
torrent::torrents_client,
|
||||
torrent_manager::{
|
||||
SearchItem, SearchRequest, SearchResponse, torrent_manager_server::TorrentManager,
|
||||
},
|
||||
},
|
||||
indexer::{Indexer, SearchResult},
|
||||
torrent::TorrentsService,
|
||||
};
|
||||
|
||||
pub struct TorrentMananagerService {
|
||||
torrents_service: TorrentsService,
|
||||
indexer: Arc<dyn Indexer>,
|
||||
}
|
||||
|
||||
impl TorrentMananagerService {
|
||||
pub fn new(torrents_service: TorrentsService, indexer: Arc<dyn Indexer>) -> Self {
|
||||
return Self {
|
||||
torrents_service,
|
||||
indexer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl TorrentManager for TorrentMananagerService {
|
||||
async fn search(
|
||||
&self,
|
||||
request: tonic::Request<SearchRequest>,
|
||||
) -> Result<tonic::Response<SearchResponse>, tonic::Status> {
|
||||
let name: String = request.into_inner().name;
|
||||
let search_items: Vec<SearchItem> = self
|
||||
.indexer
|
||||
.search(&name)
|
||||
.await
|
||||
.unwrap_or(Vec::new())
|
||||
.iter()
|
||||
.map(|item| {
|
||||
return SearchItem {
|
||||
name: item.title.clone(),
|
||||
url: item.link.clone(),
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SearchResponse { item: search_items };
|
||||
|
||||
return tonic::Result::Ok(tonic::Response::new(response));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user