feat: add indexer and torrent REST controllers with service layer

- Add config module for yaml config (database, indexers, torrent)
- Add indexer module with Torznab protocol support
- Add IndexerService and TorrentService for business logic
- Add REST controllers for indexer search and torrent management
- Add Docker Compose for PostgreSQL and Jackett
- Add ERD documentation for database schema
This commit is contained in:
Alexander
2026-04-28 18:53:50 +02:00
parent f77806ba46
commit 1aaaab4640
23 changed files with 1841 additions and 51 deletions
+95
View File
@@ -0,0 +1,95 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::config::IndexerConfig;
use crate::indexer::{Indexer, IndexerError, MusicSearchCriteria, SearchResult, TorznabIndexer};
pub struct IndexerService {
indexers: HashMap<String, Arc<dyn Indexer>>,
}
impl IndexerService {
pub fn new() -> Self {
Self {
indexers: HashMap::new(),
}
}
pub fn from_config(configs: &[IndexerConfig]) -> Result<Self, IndexerError> {
let mut service = Self::new();
for config in configs {
let indexer = TorznabIndexer::new(&config.name, &config.url, &config.api_key)?;
service.add_indexer(Arc::new(indexer));
}
Ok(service)
}
pub fn add_indexer(&mut self, indexer: Arc<dyn Indexer>) {
self.indexers.insert(indexer.name().to_string(), indexer);
}
pub fn get_indexer(&self, name: &str) -> Option<Arc<dyn Indexer>> {
self.indexers.get(name).cloned()
}
pub fn list_indexers(&self) -> Vec<IndexerInfo> {
self.indexers
.values()
.map(|i| IndexerInfo {
name: i.name().to_string(),
supports_music: i.supports_music_search(),
})
.collect()
}
pub async fn search(
&self,
criteria: &MusicSearchCriteria,
indexer_name: Option<&str>,
) -> Result<Vec<SearchResult>, IndexerError> {
match indexer_name {
Some(name) => {
let indexer = self.indexers.get(name).ok_or_else(|| {
IndexerError::Unavailable(format!("indexer not found: {}", name))
})?;
indexer.search(criteria).await
}
None => {
let mut all_results = Vec::new();
for indexer in self.indexers.values() {
if indexer.supports_music_search() {
match indexer.search(criteria).await {
Ok(results) => all_results.extend(results),
Err(e) => {
tracing::warn!("indexer {} failed: {}", indexer.name(), e);
}
}
}
}
Ok(all_results)
}
}
}
pub async fn test_indexer(&self, name: &str) -> Result<(), IndexerError> {
let indexer = self
.indexers
.get(name)
.ok_or_else(|| IndexerError::Unavailable(format!("indexer not found: {}", name)))?;
indexer.test_connection().await
}
}
impl Default for IndexerService {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct IndexerInfo {
pub name: String,
pub supports_music: bool,
}
+6
View File
@@ -1,3 +1,9 @@
mod indexer_service;
mod torrent_service;
pub use indexer_service::{IndexerInfo, IndexerService};
pub use torrent_service::TorrentService;
use uuid::Uuid;
use crate::models::Track;
+81
View File
@@ -0,0 +1,81 @@
use std::sync::Arc;
use crate::config::QBittorrentConfig;
use crate::torrent::{QBittorrentClient, TorrentClient, TorrentClientError, TorrentInfo};
pub struct TorrentService {
client: Option<Arc<dyn TorrentClient>>,
}
impl TorrentService {
pub fn new() -> Self {
Self { client: None }
}
pub async fn from_qbittorrent_config(
config: &QBittorrentConfig,
) -> Result<Self, TorrentClientError> {
let mut client = QBittorrentClient::new(&config.url, &config.username, &config.password)?;
client.connect().await?;
Ok(Self {
client: Some(Arc::new(client)),
})
}
fn client(&self) -> Result<&Arc<dyn TorrentClient>, TorrentClientError> {
self.client
.as_ref()
.ok_or_else(|| TorrentClientError::ConnectionFailed("no client configured".into()))
}
pub async fn is_connected(&self) -> bool {
self.client.is_some()
}
pub async fn list_torrents(&self) -> Result<Vec<TorrentInfo>, TorrentClientError> {
self.client()?.list_torrents().await
}
pub async fn get_torrent(&self, hash: &str) -> Result<TorrentInfo, TorrentClientError> {
self.client()?.get_torrent(hash).await
}
pub async fn add_torrent_url(
&self,
url: &str,
save_path: Option<&str>,
) -> Result<(), TorrentClientError> {
self.client()?.add_torrent_url(url, save_path).await
}
pub async fn add_torrent_file(
&self,
data: &[u8],
save_path: Option<&str>,
) -> Result<(), TorrentClientError> {
self.client()?.add_torrent_file(data, save_path).await
}
pub async fn remove_torrent(
&self,
hash: &str,
delete_files: bool,
) -> Result<(), TorrentClientError> {
self.client()?.remove_torrent(hash, delete_files).await
}
pub async fn pause_torrent(&self, hash: &str) -> Result<(), TorrentClientError> {
self.client()?.pause_torrent(hash).await
}
pub async fn resume_torrent(&self, hash: &str) -> Result<(), TorrentClientError> {
self.client()?.resume_torrent(hash).await
}
}
impl Default for TorrentService {
fn default() -> Self {
Self::new()
}
}