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:
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user