watchlist backfill
Some checks failed
CI / Check / Test (push) Failing after 57s
CI / Release build (push) Has been skipped

This commit is contained in:
2026-05-28 03:52:38 +02:00
parent b3e7a42d2f
commit 51bd580a04
22 changed files with 515 additions and 133 deletions

View File

@@ -18,7 +18,9 @@ impl ApObjectHandler for CompositeObjectHandler {
&self,
user_id: uuid::Uuid,
) -> anyhow::Result<Vec<(Url, serde_json::Value)>> {
self.review.get_local_objects_for_user(user_id).await
let mut results = self.review.get_local_objects_for_user(user_id).await?;
results.extend(self.watchlist.get_local_objects_for_user(user_id).await?);
Ok(results)
}
async fn get_local_objects_page(

View File

@@ -3,7 +3,7 @@ use domain::ports::EventHandler;
use domain::{
errors::DomainError,
events::DomainEvent,
ports::{MovieRepository, ReviewRepository},
ports::LocalApContentQuery,
value_objects::{ReviewId, UserId},
};
use std::sync::Arc;
@@ -15,22 +15,19 @@ use crate::urls::{actor_url, review_url};
pub struct ActivityPubEventHandler {
ap_service: Arc<ActivityPubService>,
movie_repository: Arc<dyn MovieRepository>,
review_repository: Arc<dyn ReviewRepository>,
content_query: Arc<dyn LocalApContentQuery>,
base_url: String,
}
impl ActivityPubEventHandler {
pub fn new(
ap_service: Arc<ActivityPubService>,
movie_repository: Arc<dyn MovieRepository>,
review_repository: Arc<dyn ReviewRepository>,
content_query: Arc<dyn LocalApContentQuery>,
base_url: String,
) -> Self {
Self {
ap_service,
movie_repository,
review_repository,
content_query,
base_url,
}
}
@@ -90,7 +87,7 @@ impl EventHandler for ActivityPubEventHandler {
impl ActivityPubEventHandler {
async fn on_review_logged(&self, user_id: &UserId, review_id: &ReviewId) -> anyhow::Result<()> {
let review = match self.review_repository.get_review_by_id(review_id).await? {
let review = match self.content_query.get_review_by_id(review_id).await? {
Some(r) => r,
None => return Ok(()),
};
@@ -99,7 +96,7 @@ impl ActivityPubEventHandler {
let actor = actor_url(&self.base_url, user_id.value());
let movie = self
.movie_repository
.content_query
.get_movie_by_id(review.movie_id())
.await
.ok()
@@ -108,10 +105,7 @@ impl ActivityPubEventHandler {
.as_ref()
.map(|m| m.title().value().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let release_year = movie
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let release_year = movie.as_ref().map(|m| m.release_year().value()).unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
@@ -140,7 +134,7 @@ impl ActivityPubEventHandler {
user_id: &UserId,
review_id: &ReviewId,
) -> anyhow::Result<()> {
let review = match self.review_repository.get_review_by_id(review_id).await? {
let review = match self.content_query.get_review_by_id(review_id).await? {
Some(r) => r,
None => return Ok(()),
};
@@ -149,7 +143,7 @@ impl ActivityPubEventHandler {
let actor = actor_url(&self.base_url, user_id.value());
let movie = self
.movie_repository
.content_query
.get_movie_by_id(review.movie_id())
.await
.ok()
@@ -158,10 +152,7 @@ impl ActivityPubEventHandler {
.as_ref()
.map(|m| m.title().value().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let release_year = movie
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let release_year = movie.as_ref().map(|m| m.release_year().value()).unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
@@ -211,7 +202,7 @@ impl ActivityPubEventHandler {
let actor = actor_url(&self.base_url, user_id.value());
let poster_url = self
.movie_repository
.content_query
.get_movie_by_id(movie_id)
.await
.ok()

View File

@@ -30,22 +30,21 @@ pub async fn wire(
federation_repo: std::sync::Arc<dyn FederationRepository>,
review_store: std::sync::Arc<dyn RemoteReviewRepository>,
remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
movie_repo: std::sync::Arc<dyn domain::ports::MovieRepository>,
review_repo: std::sync::Arc<dyn domain::ports::ReviewRepository>,
diary_repo: std::sync::Arc<dyn domain::ports::DiaryRepository>,
base_url: String,
allow_registration: bool,
_event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
) -> anyhow::Result<ActivityPubWire> {
let review_handler = std::sync::Arc::new(ReviewObjectHandler {
movie_repository: std::sync::Arc::clone(&movie_repo),
diary_repository: std::sync::Arc::clone(&diary_repo),
content_query: std::sync::Arc::clone(&local_ap_content),
review_store,
base_url: base_url.clone(),
});
let watchlist_handler = std::sync::Arc::new(watchlist_handler::WatchlistObjectHandler {
remote_watchlist_repo,
content_query: std::sync::Arc::clone(&local_ap_content),
base_url: base_url.clone(),
});
let composite = std::sync::Arc::new(composite_handler::CompositeObjectHandler {
review: review_handler,
@@ -80,8 +79,7 @@ pub async fn wire(
let router = concrete.router();
let event_handler = std::sync::Arc::new(ActivityPubEventHandler::new(
std::sync::Arc::clone(&concrete),
movie_repo,
review_repo,
local_ap_content,
base_url,
)) as std::sync::Arc<dyn domain::ports::EventHandler>;

View File

@@ -3,8 +3,8 @@ use std::sync::Arc;
use k_ap::ApObjectHandler;
use async_trait::async_trait;
use domain::{
models::{Review, ReviewSource},
ports::{DiaryRepository, MovieRepository},
models::ReviewSource,
ports::LocalApContentQuery,
value_objects::{Comment, MovieId, Rating, ReviewId, UserId},
};
use url::Url;
@@ -14,8 +14,7 @@ use crate::remote_review_repository::RemoteReviewRepository;
use crate::urls::{actor_url, review_url};
pub struct ReviewObjectHandler {
pub movie_repository: Arc<dyn MovieRepository>,
pub diary_repository: Arc<dyn DiaryRepository>,
pub content_query: Arc<dyn LocalApContentQuery>,
pub review_store: Arc<dyn RemoteReviewRepository>,
pub base_url: String,
}
@@ -27,51 +26,33 @@ impl ApObjectHandler for ReviewObjectHandler {
user_id: uuid::Uuid,
) -> anyhow::Result<Vec<(Url, serde_json::Value)>> {
let domain_user_id = UserId::from_uuid(user_id);
let history = self
.diary_repository
.get_user_history(&domain_user_id)
.await?;
let entries = self
.content_query
.get_local_reviews_for_user(&domain_user_id)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let actor = actor_url(&self.base_url, user_id);
let mut results = Vec::new();
for entry in history {
for entry in entries {
let review = entry.review();
if !matches!(review.source(), ReviewSource::Local) {
continue;
}
let movie = entry.movie();
let ap_id = review_url(&self.base_url, review.id());
let actor_url = actor_url(&self.base_url, user_id);
let movie = self
.movie_repository
.get_movie_by_id(review.movie_id())
.await
.ok()
.flatten();
let movie_title = movie
.as_ref()
.map(|m| m.title().value().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let release_year = movie
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
.poster_path()
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = review_to_ap_object(
review,
ap_id.clone(),
actor_url,
movie_title,
release_year,
actor.clone(),
movie.title().value().to_string(),
movie.release_year().value(),
poster_url,
&self.base_url,
);
let json = serde_json::to_value(obj)?;
results.push((ap_id, json));
results.push((ap_id, serde_json::to_value(obj)?));
}
Ok(results)
}
@@ -82,23 +63,18 @@ impl ApObjectHandler for ReviewObjectHandler {
before: Option<chrono::DateTime<chrono::Utc>>,
limit: usize,
) -> anyhow::Result<Vec<(url::Url, serde_json::Value, chrono::DateTime<chrono::Utc>)>> {
use domain::value_objects::UserId;
let domain_user_id = UserId::from_uuid(user_id);
let history = self
.diary_repository
.get_user_history(&domain_user_id)
.await?;
let entries = self
.content_query
.get_local_reviews_for_user(&domain_user_id)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let actor = actor_url(&self.base_url, user_id);
let mut results = Vec::new();
for entry in history {
for entry in entries {
let review = entry.review();
if !matches!(review.source(), ReviewSource::Local) {
continue;
}
let published =
chrono::DateTime::from_naive_utc_and_offset(*review.watched_at(), chrono::Utc);
let published = chrono::DateTime::from_naive_utc_and_offset(*review.watched_at(), chrono::Utc);
if let Some(cutoff) = before
&& published >= cutoff
@@ -106,39 +82,22 @@ impl ApObjectHandler for ReviewObjectHandler {
continue;
}
let movie = entry.movie();
let ap_id = review_url(&self.base_url, review.id());
let actor_url = actor_url(&self.base_url, user_id);
let movie = self
.movie_repository
.get_movie_by_id(review.movie_id())
.await
.ok()
.flatten();
let movie_title = movie
.as_ref()
.map(|m| m.title().value().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let release_year = movie
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
.poster_path()
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = review_to_ap_object(
review,
ap_id.clone(),
actor_url,
movie_title,
release_year,
actor.clone(),
movie.title().value().to_string(),
movie.release_year().value(),
poster_url,
&self.base_url,
);
let json = serde_json::to_value(obj)?;
results.push((ap_id, json, published));
results.push((ap_id, serde_json::to_value(obj)?, published));
if results.len() >= limit {
break;
@@ -174,7 +133,7 @@ impl ApObjectHandler for ReviewObjectHandler {
let rating = Rating::new(obj.rating.min(5))?;
let comment = obj.comment.map(Comment::new).transpose()?;
let review = Review::from_persistence(
let review = domain::models::Review::from_persistence(
review_id,
movie_id,
user_id,
@@ -242,7 +201,7 @@ impl ApObjectHandler for ReviewObjectHandler {
}
async fn count_local_posts(&self) -> anyhow::Result<u64> {
self.diary_repository
self.content_query
.count_local_posts()
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))

View File

@@ -3,22 +3,61 @@ use std::sync::Arc;
use k_ap::ApObjectHandler;
use async_trait::async_trait;
use chrono::Utc;
use domain::{models::RemoteWatchlistEntry, ports::RemoteWatchlistRepository};
use domain::{
models::RemoteWatchlistEntry,
ports::{LocalApContentQuery, RemoteWatchlistRepository},
value_objects::UserId,
};
use url::Url;
use crate::objects::WatchlistObject;
use crate::{objects::{WatchlistObject, watchlist_to_ap_object}, urls::{actor_url, watchlist_entry_url}};
pub struct WatchlistObjectHandler {
pub remote_watchlist_repo: Arc<dyn RemoteWatchlistRepository>,
pub content_query: Arc<dyn LocalApContentQuery>,
pub base_url: String,
}
#[async_trait]
impl ApObjectHandler for WatchlistObjectHandler {
async fn get_local_objects_for_user(
&self,
_user_id: uuid::Uuid,
user_id: uuid::Uuid,
) -> anyhow::Result<Vec<(Url, serde_json::Value)>> {
Ok(vec![])
let domain_user_id = UserId::from_uuid(user_id);
let entries = self
.content_query
.get_local_watchlist_for_user(&domain_user_id)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let actor = actor_url(&self.base_url, user_id);
let mut results = Vec::new();
for wm in entries {
let movie_id = wm.entry.movie_id.value();
let ap_id = watchlist_entry_url(&self.base_url, user_id, movie_id);
let added_at = chrono::DateTime::from_naive_utc_and_offset(wm.entry.added_at, Utc);
let external_metadata_id = wm
.movie
.external_metadata_id()
.map(|id| id.value().to_string());
let poster_url = wm
.movie
.poster_path()
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = watchlist_to_ap_object(
ap_id.clone(),
actor.clone(),
wm.movie.title().value().to_string(),
wm.movie.release_year().value(),
external_metadata_id,
poster_url,
added_at,
&self.base_url,
);
results.push((ap_id, serde_json::to_value(obj)?));
}
Ok(results)
}
async fn get_local_objects_page(