feat: rename product to Screened, add PRODUCT_NAME constant to domain
This commit is contained in:
@@ -53,7 +53,7 @@ EVENT_BUS_BACKEND=db
|
|||||||
# EVENT_BUS_BACKEND=nats
|
# EVENT_BUS_BACKEND=nats
|
||||||
# NATS_URL=nats://localhost:4222
|
# NATS_URL=nats://localhost:4222
|
||||||
# NATS_MODE=jetstream # "jetstream" (default, at-least-once) or "core" (fire-and-forget)
|
# NATS_MODE=jetstream # "jetstream" (default, at-least-once) or "core" (fire-and-forget)
|
||||||
# NATS_SUBJECT_PREFIX=movies-diary.events
|
# NATS_SUBJECT_PREFIX=screened.events
|
||||||
# NATS_STREAM_NAME=MOVIES_DIARY_EVENTS
|
# NATS_STREAM_NAME=MOVIES_DIARY_EVENTS
|
||||||
# NATS_CONSUMER_NAME=worker
|
# NATS_CONSUMER_NAME=worker
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Movies Diary
|
# Screened
|
||||||
|
|
||||||
A self-hosted, server-side rendered movie logging system with a full REST API. Built in Rust — no JavaScript in the HTML interface, just HTML forms and an RSS feed. Designed to run as a lightweight widget embedded on a personal site or as a backend for third-party clients.
|
A self-hosted, server-side rendered movie logging system with a full REST API. Built in Rust — no JavaScript in the HTML interface, just HTML forms and an RSS feed. Designed to run as a lightweight widget embedded on a personal site or as a backend for third-party clients.
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ A self-hosted, server-side rendered movie logging system with a full REST API. B
|
|||||||
- Movie enrichment via TMDb — full cast, crew, genres, keywords, runtime, budget/revenue, ratings; fetched automatically on movie discovery and refreshed every 30 days; exposed via `GET /api/v1/movies/{id}/profile`
|
- Movie enrichment via TMDb — full cast, crew, genres, keywords, runtime, budget/revenue, ratings; fetched automatically on movie discovery and refreshed every 30 days; exposed via `GET /api/v1/movies/{id}/profile`
|
||||||
- RSS/Atom feed for public subscription (global and per-user)
|
- RSS/Atom feed for public subscription (global and per-user)
|
||||||
- JWT authentication via cookie (HTML) or Bearer token (REST API)
|
- JWT authentication via cookie (HTML) or Bearer token (REST API)
|
||||||
- ActivityPub federation — follow/unfollow remote users, accept/reject/remove followers, federated reviews broadcast as `Note` objects with `#MoviesDiary` + `#MovieTitle` hashtags, paginated outbox, boost/Announce tracking, NodeInfo discovery endpoint, shared inbox delivery, actor profile sync (bio, avatar, discoverable)
|
- ActivityPub federation — follow/unfollow remote users, accept/reject/remove followers, federated reviews broadcast as `Note` objects with `#Screened` + `#MovieTitle` hashtags, paginated outbox, boost/Announce tracking, NodeInfo discovery endpoint, shared inbox delivery, actor profile sync (bio, avatar, discoverable)
|
||||||
- Federation moderation — instance-level domain blocking (admin-managed), per-user actor blocking with `Block` activity, delivery filter excludes blocked actors and blocked-domain inboxes
|
- Federation moderation — instance-level domain blocking (admin-managed), per-user actor blocking with `Block` activity, delivery filter excludes blocked actors and blocked-domain inboxes
|
||||||
- CSV and JSON diary export
|
- CSV and JSON diary export
|
||||||
- File importer: upload CSV, TSV, JSON, or XLSX from any source (Letterboxd, IMDb, etc.), map columns to domain fields via a step-by-step wizard or REST API, save mapping profiles for repeat imports
|
- File importer: upload CSV, TSV, JSON, or XLSX from any source (Letterboxd, IMDb, etc.), map columns to domain fields via a step-by-step wizard or REST API, save mapping profiles for repeat imports
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ pub mod review_handler;
|
|||||||
pub(crate) mod urls;
|
pub(crate) mod urls;
|
||||||
pub mod user_adapter;
|
pub mod user_adapter;
|
||||||
|
|
||||||
|
use domain::PRODUCT_NAME;
|
||||||
|
|
||||||
// Re-export the generic base types that callers need
|
// Re-export the generic base types that callers need
|
||||||
pub use activitypub_base::{
|
pub use activitypub_base::{
|
||||||
ActivityPubService, ApFederationConfig, ApObjectHandler, ApUser, ApUserRepository,
|
ActivityPubService, ApFederationConfig, ApObjectHandler, ApUser, ApUserRepository,
|
||||||
@@ -46,7 +48,7 @@ pub async fn wire(
|
|||||||
}),
|
}),
|
||||||
base_url.clone(),
|
base_url.clone(),
|
||||||
allow_registration,
|
allow_registration,
|
||||||
"movies-diary".to_string(),
|
PRODUCT_NAME.to_lowercase(),
|
||||||
cfg!(debug_assertions),
|
cfg!(debug_assertions),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ pub fn review_to_ap_object(
|
|||||||
let tag = vec![
|
let tag = vec![
|
||||||
ApHashtag {
|
ApHashtag {
|
||||||
kind: "Hashtag".to_string(),
|
kind: "Hashtag".to_string(),
|
||||||
href: Url::parse(&format!("{}/tags/moviesdiary", base_url))
|
href: Url::parse(&format!("{}/tags/screened", base_url))
|
||||||
.expect("valid base_url"),
|
.expect("valid base_url"),
|
||||||
name: "#MoviesDiary".to_string(),
|
name: "#Screened".to_string(),
|
||||||
},
|
},
|
||||||
ApHashtag {
|
ApHashtag {
|
||||||
kind: "Hashtag".to_string(),
|
kind: "Hashtag".to_string(),
|
||||||
@@ -137,7 +137,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(obj.tag.len(), 2);
|
assert_eq!(obj.tag.len(), 2);
|
||||||
let names: Vec<&str> = obj.tag.iter().map(|t| t.name.as_str()).collect();
|
let names: Vec<&str> = obj.tag.iter().map(|t| t.name.as_str()).collect();
|
||||||
assert!(names.contains(&"#MoviesDiary"));
|
assert!(names.contains(&"#Screened"));
|
||||||
assert!(names.contains(&"#Dune"));
|
assert!(names.contains(&"#Dune"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ impl NatsConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let subject_prefix = std::env::var("NATS_SUBJECT_PREFIX")
|
let subject_prefix = std::env::var("NATS_SUBJECT_PREFIX")
|
||||||
.unwrap_or_else(|_| "movies-diary.events".to_string());
|
.unwrap_or_else(|_| "screened.events".to_string());
|
||||||
let stream_name = std::env::var("NATS_STREAM_NAME")
|
let stream_name = std::env::var("NATS_STREAM_NAME")
|
||||||
.unwrap_or_else(|_| "MOVIES_DIARY_EVENTS".to_string());
|
.unwrap_or_else(|_| "SCREENED_EVENTS".to_string());
|
||||||
let consumer_name = std::env::var("NATS_CONSUMER_NAME")
|
let consumer_name = std::env::var("NATS_CONSUMER_NAME")
|
||||||
.unwrap_or_else(|_| "worker".to_string());
|
.unwrap_or_else(|_| "worker".to_string());
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ mod tests {
|
|||||||
let cfg = NatsConfig::from_env().unwrap();
|
let cfg = NatsConfig::from_env().unwrap();
|
||||||
assert_eq!(cfg.url, "nats://localhost:4222");
|
assert_eq!(cfg.url, "nats://localhost:4222");
|
||||||
assert_eq!(cfg.mode, NatsMode::JetStream);
|
assert_eq!(cfg.mode, NatsMode::JetStream);
|
||||||
assert_eq!(cfg.subject_prefix, "movies-diary.events");
|
assert_eq!(cfg.subject_prefix, "screened.events");
|
||||||
assert_eq!(cfg.stream_name, "MOVIES_DIARY_EVENTS");
|
assert_eq!(cfg.stream_name, "SCREENED_EVENTS");
|
||||||
assert_eq!(cfg.consumer_name, "worker");
|
assert_eq!(cfg.consumer_name, "worker");
|
||||||
|
|
||||||
unsafe { std::env::remove_var("NATS_URL"); }
|
unsafe { std::env::remove_var("NATS_URL"); }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
content="A personal movie diary — track what you watch, rate and review films."
|
content="A personal movie diary — track what you watch, rate and review films."
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="Movies Diary" />
|
<meta property="og:site_name" content="Screened" />
|
||||||
<meta property="og:title" content="{{ ctx.page_title }}" />
|
<meta property="og:title" content="{{ ctx.page_title }}" />
|
||||||
<meta property="og:url" content="{{ ctx.canonical_url }}" />
|
<meta property="og:url" content="{{ ctx.canonical_url }}" />
|
||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<a href="/" class="site-title">Movies Diary</a>
|
<a href="/" class="site-title">Screened</a>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">Feed</a>
|
<a href="/">Feed</a>
|
||||||
<a href="/users">Users</a>
|
<a href="/users">Users</a>
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ pub mod models;
|
|||||||
pub mod ports;
|
pub mod ports;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
pub mod value_objects;
|
pub mod value_objects;
|
||||||
|
|
||||||
|
pub const PRODUCT_NAME: &str = "Screened";
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub(crate) async fn build_page_context(
|
|||||||
is_admin,
|
is_admin,
|
||||||
register_enabled: state.app_ctx.config.allow_registration,
|
register_enabled: state.app_ctx.config.allow_registration,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Movies Diary".to_string(),
|
page_title: domain::PRODUCT_NAME.to_string(),
|
||||||
canonical_url: state.app_ctx.config.base_url.clone(),
|
canonical_url: state.app_ctx.config.base_url.clone(),
|
||||||
csrf_token,
|
csrf_token,
|
||||||
page_rss_url: None,
|
page_rss_url: None,
|
||||||
@@ -108,7 +108,7 @@ pub async fn get_login_page(
|
|||||||
is_admin: false,
|
is_admin: false,
|
||||||
register_enabled: state.app_ctx.config.allow_registration,
|
register_enabled: state.app_ctx.config.allow_registration,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Login — Movies Diary".to_string(),
|
page_title: format!("Login — {}", domain::PRODUCT_NAME),
|
||||||
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
||||||
csrf_token: csrf.0,
|
csrf_token: csrf.0,
|
||||||
page_rss_url: None,
|
page_rss_url: None,
|
||||||
@@ -175,7 +175,7 @@ pub async fn get_register_page(
|
|||||||
is_admin: false,
|
is_admin: false,
|
||||||
register_enabled: true,
|
register_enabled: true,
|
||||||
rss_url: "/feed.rss".to_string(),
|
rss_url: "/feed.rss".to_string(),
|
||||||
page_title: "Register — Movies Diary".to_string(),
|
page_title: format!("Register — {}", domain::PRODUCT_NAME),
|
||||||
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
||||||
csrf_token: csrf.0,
|
csrf_token: csrf.0,
|
||||||
page_rss_url: None,
|
page_rss_url: None,
|
||||||
@@ -236,7 +236,7 @@ pub async fn get_new_review_page(
|
|||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
||||||
ctx.page_title = "Log a Review — Movies Diary".to_string();
|
ctx.page_title = format!("Log a Review — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
|
||||||
let html = state
|
let html = state
|
||||||
.html_renderer
|
.html_renderer
|
||||||
@@ -450,7 +450,7 @@ pub async fn get_users_list(
|
|||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, user_id, csrf.0).await;
|
let mut ctx = build_page_context(&state, user_id, csrf.0).await;
|
||||||
ctx.page_title = "Members — Movies Diary".to_string();
|
ctx.page_title = format!("Members — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||||
|
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
@@ -575,7 +575,7 @@ pub async fn get_user_profile(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let display_name = profile_user.username().value();
|
let display_name = profile_user.username().value();
|
||||||
ctx.page_title = format!("{}'s Diary — Movies Diary", display_name);
|
ctx.page_title = format!("{}'s Diary — {}", display_name, domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!(
|
ctx.canonical_url = format!(
|
||||||
"{}/users/{}",
|
"{}/users/{}",
|
||||||
state.app_ctx.config.base_url, profile_user_uuid
|
state.app_ctx.config.base_url, profile_user_uuid
|
||||||
@@ -822,7 +822,7 @@ pub async fn get_following_page(
|
|||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Following — Movies Diary".to_string();
|
ctx.page_title = format!("Following — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!(
|
ctx.canonical_url = format!(
|
||||||
"{}/users/{}/following-list",
|
"{}/users/{}/following-list",
|
||||||
state.app_ctx.config.base_url, profile_user_uuid
|
state.app_ctx.config.base_url, profile_user_uuid
|
||||||
@@ -872,7 +872,7 @@ pub async fn get_followers_page(
|
|||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
}
|
}
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Followers — Movies Diary".to_string();
|
ctx.page_title = format!("Followers — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!(
|
ctx.canonical_url = format!(
|
||||||
"{}/users/{}/followers-list",
|
"{}/users/{}/followers-list",
|
||||||
state.app_ctx.config.base_url, profile_user_uuid
|
state.app_ctx.config.base_url, profile_user_uuid
|
||||||
@@ -1006,7 +1006,7 @@ pub async fn get_profile_settings(
|
|||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Profile Settings — Movies Diary".to_string();
|
ctx.page_title = format!("Profile Settings — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!("{}/settings/profile", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/settings/profile", state.app_ctx.config.base_url);
|
||||||
|
|
||||||
let user = match state
|
let user = match state
|
||||||
@@ -1061,7 +1061,7 @@ pub async fn get_blocked_domains_page(
|
|||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
||||||
ctx.page_title = "Blocked Domains — Movies Diary".to_string();
|
ctx.page_title = format!("Blocked Domains — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
|
||||||
match state.ap_service.get_blocked_domains().await {
|
match state.ap_service.get_blocked_domains().await {
|
||||||
Ok(domains) => {
|
Ok(domains) => {
|
||||||
@@ -1134,7 +1134,7 @@ pub async fn get_blocked_actors_page(
|
|||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||||
ctx.page_title = "Blocked Users — Movies Diary".to_string();
|
ctx.page_title = format!("Blocked Users — {}", domain::PRODUCT_NAME);
|
||||||
ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url);
|
ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url);
|
||||||
match state.ap_service.get_blocked_actors(user_id.value()).await {
|
match state.ap_service.get_blocked_actors(user_id.value()).await {
|
||||||
Ok(actors) => {
|
Ok(actors) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod social;
|
|||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use domain::PRODUCT_NAME;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
Modify, OpenApi,
|
Modify, OpenApi,
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||||
@@ -28,9 +29,9 @@ impl Modify for SecurityAddon {
|
|||||||
fn build() -> utoipa::openapi::OpenApi {
|
fn build() -> utoipa::openapi::OpenApi {
|
||||||
let mut api = auth::AuthDoc::openapi();
|
let mut api = auth::AuthDoc::openapi();
|
||||||
api.info = utoipa::openapi::InfoBuilder::new()
|
api.info = utoipa::openapi::InfoBuilder::new()
|
||||||
.title("Movies Diary API")
|
.title(format!("{} API", PRODUCT_NAME))
|
||||||
.version("1.0.0")
|
.version("1.0.0")
|
||||||
.description(Some("REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token."))
|
.description(Some(format!("REST API for {}. Authenticate with `POST /api/v1/auth/login` to get a Bearer token.", PRODUCT_NAME)))
|
||||||
.build();
|
.build();
|
||||||
api.merge(diary::DiaryDoc::openapi());
|
api.merge(diary::DiaryDoc::openapi());
|
||||||
api.merge(movies::MoviesDoc::openapi());
|
api.merge(movies::MoviesDoc::openapi());
|
||||||
|
|||||||
Reference in New Issue
Block a user