Files
movies-diary/crates/application/src/ports.rs
Gabriel Kaszewski aadad3cfb0
Some checks failed
CI / Check / Test (push) Failing after 6m5s
feat: Jellyfin/Plex auto-import via watch queue
Webhook ingestion from media servers — movies land in a pending
watch queue, user rates and confirms to create diary entries.

- domain: WatchEvent, WebhookToken models, MediaServerParser port
- adapters: jellyfin + plex parser crates, SQLite + Postgres repos
- application: ingest/confirm/dismiss/cleanup use cases, token mgmt
- presentation: webhook endpoints (bearer + query param auth),
  watch queue + integrations settings HTML pages, OpenAPI docs
- worker: WatchEventCleanupJob (daily, 30d retention)

Movie resolution deferred to confirm — single canonical path
through log_review for enrichment, poster fetch, federation.
2026-06-02 17:34:16 +02:00

269 lines
7.5 KiB
Rust

use uuid::Uuid;
use domain::models::{
DiaryEntry, FeedEntry, MonthActivity, Movie, MovieProfile, MovieStats, UserStats, UserSummary,
UserTrends, collections::Paginated,
};
pub struct RemoteActorView {
pub handle: String,
pub display_name: Option<String>,
pub url: String,
pub avatar_url: Option<String>,
}
pub struct HtmlPageContext {
pub user_email: Option<String>,
pub user_id: Option<Uuid>,
pub is_admin: bool,
pub register_enabled: bool,
pub rss_url: String,
pub page_title: String,
pub canonical_url: String,
pub csrf_token: String,
pub page_rss_url: Option<String>,
}
impl HtmlPageContext {
pub fn is_current_user(&self, id: Uuid) -> bool {
self.user_id == Some(id)
}
}
pub struct LoginPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct RegisterPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct NewReviewPageData<'a> {
pub ctx: HtmlPageContext,
pub error: Option<&'a str>,
}
pub struct ActivityFeedPageData {
pub ctx: HtmlPageContext,
pub entries: Paginated<FeedEntry>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub filter: String,
pub sort_by: String,
pub search: String,
}
pub struct UsersPageData {
pub ctx: HtmlPageContext,
pub users: Vec<UserSummary>,
pub remote_actors: Vec<RemoteActorView>,
}
pub struct ProfilePageData {
pub ctx: HtmlPageContext,
pub profile_user_id: Uuid,
pub profile_user_email: String,
pub stats: UserStats,
pub view: String,
pub entries: Option<Paginated<DiaryEntry>>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub history: Option<Vec<MonthActivity>>,
pub trends: Option<UserTrends>,
pub is_own_profile: bool,
pub error: Option<String>,
pub following_count: usize,
pub followers_count: usize,
pub pending_followers: Vec<RemoteActorView>,
pub sort_by: String,
pub search: String,
}
pub struct FollowingPageData {
pub ctx: HtmlPageContext,
pub user_id: Uuid,
pub actors: Vec<RemoteActorView>,
pub error: Option<String>,
}
pub struct FollowersPageData {
pub ctx: HtmlPageContext,
pub user_id: Uuid,
pub actors: Vec<RemoteActorView>,
pub error: Option<String>,
}
pub struct MovieDetailPageData {
pub ctx: HtmlPageContext,
pub movie: Movie,
pub stats: MovieStats,
pub reviews: Paginated<FeedEntry>,
pub profile: Option<MovieProfile>,
pub on_watchlist: bool,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub histogram_max: u64,
}
#[derive(Clone, Debug)]
pub struct WatchlistDisplayEntry {
/// Always a full URL: /images/{path} for local, https://... for remote
pub poster_url: Option<String>,
pub movie_title: String,
pub release_year: u16,
/// /movies/{id} for local; None for remote entries without a local movie record
pub movie_url: Option<String>,
pub added_at: String,
/// /watchlist/{movie_id}/remove for owner; None for remote or non-owner
pub remove_url: Option<String>,
}
pub struct WatchlistPageData {
pub ctx: HtmlPageContext,
pub owner_id: uuid::Uuid,
pub display_entries: Vec<WatchlistDisplayEntry>,
pub current_offset: u32,
pub has_more: bool,
pub limit: u32,
pub is_owner: bool,
pub error: Option<String>,
}
pub struct ImportUploadPageData {
pub ctx: HtmlPageContext,
pub profiles: Vec<ImportProfileView>,
pub error: Option<String>,
}
pub struct ImportProfileView {
pub id: String,
pub name: String,
}
pub struct ImportMappingPageData {
pub ctx: HtmlPageContext,
pub session_id: String,
pub columns: Vec<String>,
pub sample_rows: Vec<Vec<String>>,
pub domain_fields: Vec<(&'static str, &'static str)>,
pub error: Option<String>,
}
pub struct ImportPreviewRow {
pub index: usize,
pub status: ImportRowStatus,
pub cells: Vec<String>,
}
pub enum ImportRowStatus {
Valid,
Duplicate,
Invalid(String),
}
pub struct ImportPreviewPageData {
pub ctx: HtmlPageContext,
pub session_id: String,
pub columns: Vec<String>,
pub rows: Vec<ImportPreviewRow>,
}
pub struct ProfileSettingsPageData {
pub ctx: HtmlPageContext,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub banner_url: Option<String>,
pub also_known_as: Option<String>,
pub profile_fields: Vec<(String, String)>,
pub saved: bool,
}
pub struct BlockedDomainEntry {
pub domain: String,
pub reason: Option<String>,
pub blocked_at: String,
}
pub struct BlockedDomainsPageData {
pub ctx: HtmlPageContext,
pub domains: Vec<BlockedDomainEntry>,
}
pub struct BlockedActorEntry {
pub url: String,
pub handle: String,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
}
pub struct BlockedActorsPageData {
pub ctx: HtmlPageContext,
pub actors: Vec<BlockedActorEntry>,
}
pub struct WebhookTokenView {
pub id: String,
pub provider: String,
pub label: Option<String>,
pub created_at: String,
pub last_used_at: Option<String>,
}
pub struct IntegrationsPageData {
pub ctx: HtmlPageContext,
pub tokens: Vec<WebhookTokenView>,
pub webhook_base_url: String,
pub new_token: Option<String>,
}
pub struct WatchQueueDisplayEntry {
pub id: String,
pub title: String,
pub year: Option<u16>,
pub source: String,
pub watched_at: String,
pub movie_url: Option<String>,
}
pub struct WatchQueuePageData {
pub ctx: HtmlPageContext,
pub entries: Vec<WatchQueueDisplayEntry>,
pub error: Option<String>,
}
pub trait HtmlRenderer: Send + Sync {
fn render_diary_page(
&self,
data: &Paginated<DiaryEntry>,
ctx: HtmlPageContext,
) -> Result<String, String>;
fn render_login_page(&self, data: LoginPageData<'_>) -> Result<String, String>;
fn render_register_page(&self, data: RegisterPageData<'_>) -> Result<String, String>;
fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result<String, String>;
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String>;
fn render_users_page(&self, data: UsersPageData) -> Result<String, String>;
fn render_profile_page(&self, data: ProfilePageData) -> Result<String, String>;
fn render_following_page(&self, data: FollowingPageData) -> Result<String, String>;
fn render_followers_page(&self, data: FollowersPageData) -> Result<String, String>;
fn render_movie_detail_page(&self, data: MovieDetailPageData) -> Result<String, String>;
fn render_import_upload_page(&self, data: ImportUploadPageData) -> Result<String, String>;
fn render_import_mapping_page(&self, data: ImportMappingPageData) -> Result<String, String>;
fn render_import_preview_page(&self, data: ImportPreviewPageData) -> Result<String, String>;
fn render_profile_settings_page(&self, data: ProfileSettingsPageData)
-> Result<String, String>;
fn render_blocked_domains_page(&self, data: BlockedDomainsPageData) -> Result<String, String>;
fn render_blocked_actors_page(&self, data: BlockedActorsPageData) -> Result<String, String>;
fn render_watchlist_page(&self, data: WatchlistPageData) -> Result<String, String>;
fn render_integrations_page(&self, data: IntegrationsPageData) -> Result<String, String>;
fn render_watch_queue_page(&self, data: WatchQueuePageData) -> Result<String, String>;
}
pub trait RssFeedRenderer: Send + Sync {
fn render_feed(&self, entries: &[DiaryEntry], title: &str) -> Result<String, String>;
}