feat(poster-fetcher): add poster fetcher adapter with configuration and integration

This commit is contained in:
2026-05-04 11:51:20 +02:00
parent 1985d2c57f
commit edcf3c1170
8 changed files with 79 additions and 19 deletions

View File

@@ -4,6 +4,7 @@ JWT_SECRET=
JWT_TTL_SECONDS=
ALLOW_REGISTRATION=true
OMDB_API_KEY=
POSTER_FETCH_TIMEOUT_SECONDS=30
MINIO_ENDPOINT=
MINIO_ACCESS_KEY_ID=
MINIO_SECRET_ACCESS_KEY=

11
Cargo.lock generated
View File

@@ -1620,6 +1620,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "poster-fetcher"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"domain",
"reqwest 0.13.3",
]
[[package]]
name = "poster-storage"
version = "0.1.0"
@@ -1671,6 +1681,7 @@ dependencies = [
"dotenvy",
"http-body-util",
"metadata",
"poster-fetcher",
"poster-storage",
"serde",
"serde_json",

View File

@@ -1,7 +1,7 @@
[workspace]
members = [
"crates/adapters/auth",
"crates/adapters/metadata", "crates/adapters/poster-storage",
"crates/adapters/metadata", "crates/adapters/poster-fetcher", "crates/adapters/poster-storage",
"crates/adapters/rss",
"crates/adapters/sqlite",
"crates/adapters/template-askama",
@@ -39,6 +39,7 @@ application = { path = "crates/application" }
presentation = { path = "crates/presentation" }
auth = { path = "crates/adapters/auth" }
metadata = { path = "crates/adapters/metadata" }
poster-fetcher = { path = "crates/adapters/poster-fetcher" }
poster-storage = { path = "crates/adapters/poster-storage" }
rss = { path = "crates/adapters/rss" }
sqlite = { path = "crates/adapters/sqlite" }

View File

@@ -0,0 +1,10 @@
[package]
name = "poster-fetcher"
version = "0.1.0"
edition = "2021"
[dependencies]
domain = { workspace = true }
async-trait = { workspace = true }
reqwest = { workspace = true }
anyhow = { workspace = true }

View File

@@ -0,0 +1,13 @@
pub struct PosterFetcherConfig {
pub timeout_seconds: u64,
}
impl PosterFetcherConfig {
pub fn from_env() -> Self {
let timeout_seconds = std::env::var("POSTER_FETCH_TIMEOUT_SECONDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(30);
Self { timeout_seconds }
}
}

View File

@@ -0,0 +1,38 @@
mod config;
pub use config::PosterFetcherConfig;
use std::time::Duration;
use async_trait::async_trait;
use domain::{errors::DomainError, ports::PosterFetcherClient, value_objects::PosterUrl};
pub struct ReqwestPosterFetcher {
client: reqwest::Client,
}
impl ReqwestPosterFetcher {
pub fn new(config: PosterFetcherConfig) -> anyhow::Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_seconds))
.build()?;
Ok(Self { client })
}
}
#[async_trait]
impl PosterFetcherClient for ReqwestPosterFetcher {
async fn fetch_poster_bytes(&self, poster_url: &PosterUrl) -> Result<Vec<u8>, DomainError> {
let bytes = self
.client
.get(poster_url.value())
.send()
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?
.error_for_status()
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?
.bytes()
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(bytes.to_vec())
}
}

View File

@@ -23,6 +23,7 @@ domain = { workspace = true }
application = { workspace = true }
auth = { workspace = true }
metadata = { workspace = true }
poster-fetcher = { workspace = true }
poster-storage = { workspace = true }
sqlite = { workspace = true }
sqlx = { workspace = true }

View File

@@ -2,12 +2,7 @@ use std::sync::Arc;
use anyhow::Context;
use async_trait::async_trait;
use domain::{
errors::DomainError,
events::DomainEvent,
ports::{EventPublisher, PosterFetcherClient},
value_objects::PosterUrl,
};
use domain::{errors::DomainError, events::DomainEvent, ports::EventPublisher};
use sqlx::SqlitePool;
use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@@ -15,23 +10,13 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use application::{config::AppConfig, context::AppContext};
use auth::{AuthConfig, Argon2PasswordHasher, JwtAuthService};
use metadata::MetadataClientImpl;
use poster_fetcher::{PosterFetcherConfig, ReqwestPosterFetcher};
use poster_storage::{PosterStorageAdapter, StorageConfig};
use sqlite::{SqliteMovieRepository, SqliteUserRepository};
use template_askama::AskamaHtmlRenderer;
use presentation::{routes, state::AppState};
struct StubPosterFetcher;
#[async_trait]
impl PosterFetcherClient for StubPosterFetcher {
async fn fetch_poster_bytes(&self, _url: &PosterUrl) -> Result<Vec<u8>, DomainError> {
Err(DomainError::InfrastructureError(
"poster fetcher not implemented".into(),
))
}
}
struct StubEventPublisher;
#[async_trait]
@@ -81,7 +66,7 @@ async fn wire_dependencies() -> anyhow::Result<AppState> {
let app_ctx = AppContext {
repository: Arc::new(movie_repo),
metadata_client: Arc::new(MetadataClientImpl::new_omdb(omdb_api_key)),
poster_fetcher: Arc::new(StubPosterFetcher),
poster_fetcher: Arc::new(ReqwestPosterFetcher::new(PosterFetcherConfig::from_env())?),
poster_storage: Arc::new(PosterStorageAdapter::from_config(storage_config)?),
event_publisher: Arc::new(StubEventPublisher),
auth_service: Arc::new(JwtAuthService::new(auth_config)),