Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I928162008cb1ba02e1aa0e7aa971e8326a6a6964
194 lines
5.8 KiB
Rust
194 lines
5.8 KiB
Rust
use axum::{
|
|
Json,
|
|
extract::{Extension, Path, Query, State},
|
|
};
|
|
use pinakes_core::{
|
|
analytics::{UsageEvent, UsageEventType},
|
|
model::MediaId,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
auth::resolve_user_id,
|
|
dto::{
|
|
MediaResponse,
|
|
MostViewedResponse,
|
|
PaginationParams,
|
|
RecordUsageEventRequest,
|
|
WatchProgressRequest,
|
|
WatchProgressResponse,
|
|
},
|
|
error::ApiError,
|
|
state::AppState,
|
|
};
|
|
|
|
const MAX_LIMIT: u64 = 100;
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/analytics/most-viewed",
|
|
tag = "analytics",
|
|
params(
|
|
("limit" = Option<u64>, Query, description = "Maximum number of results"),
|
|
("offset" = Option<u64>, Query, description = "Pagination offset"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "Most viewed media", body = Vec<MostViewedResponse>),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn get_most_viewed(
|
|
State(state): State<AppState>,
|
|
Query(params): Query<PaginationParams>,
|
|
) -> Result<Json<Vec<MostViewedResponse>>, ApiError> {
|
|
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
|
let results = state.storage.get_most_viewed(limit).await?;
|
|
let roots = state.config.read().await.directories.roots.clone();
|
|
Ok(Json(
|
|
results
|
|
.into_iter()
|
|
.map(|(item, count)| {
|
|
MostViewedResponse {
|
|
media: MediaResponse::new(item, &roots),
|
|
view_count: count,
|
|
}
|
|
})
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/analytics/recently-viewed",
|
|
tag = "analytics",
|
|
params(
|
|
("limit" = Option<u64>, Query, description = "Maximum number of results"),
|
|
("offset" = Option<u64>, Query, description = "Pagination offset"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "Recently viewed media", body = Vec<MediaResponse>),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn get_recently_viewed(
|
|
State(state): State<AppState>,
|
|
Extension(username): Extension<String>,
|
|
Query(params): Query<PaginationParams>,
|
|
) -> Result<Json<Vec<MediaResponse>>, ApiError> {
|
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
|
let limit = params.limit.unwrap_or(20).min(MAX_LIMIT);
|
|
let items = state.storage.get_recently_viewed(user_id, limit).await?;
|
|
let roots = state.config.read().await.directories.roots.clone();
|
|
Ok(Json(
|
|
items
|
|
.into_iter()
|
|
.map(|item| MediaResponse::new(item, &roots))
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/v1/analytics/events",
|
|
tag = "analytics",
|
|
request_body = RecordUsageEventRequest,
|
|
responses(
|
|
(status = 200, description = "Event recorded"),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn record_event(
|
|
State(state): State<AppState>,
|
|
Extension(username): Extension<String>,
|
|
Json(req): Json<RecordUsageEventRequest>,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
let event_type: UsageEventType =
|
|
req.event_type.parse().map_err(|e: String| {
|
|
ApiError(pinakes_core::error::PinakesError::InvalidOperation(e))
|
|
})?;
|
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
|
let event = UsageEvent {
|
|
id: Uuid::now_v7(),
|
|
media_id: req.media_id.map(MediaId),
|
|
user_id: Some(user_id),
|
|
event_type,
|
|
timestamp: chrono::Utc::now(),
|
|
duration_secs: req.duration_secs,
|
|
context_json: req.context.map(|v| v.to_string()),
|
|
};
|
|
state.storage.record_usage_event(&event).await?;
|
|
Ok(Json(serde_json::json!({"recorded": true})))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/media/{id}/progress",
|
|
tag = "analytics",
|
|
params(
|
|
("id" = Uuid, Path, description = "Media item ID"),
|
|
),
|
|
responses(
|
|
(status = 200, description = "Watch progress", body = WatchProgressResponse),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 404, description = "Not found"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn get_watch_progress(
|
|
State(state): State<AppState>,
|
|
Extension(username): Extension<String>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<WatchProgressResponse>, ApiError> {
|
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
|
let progress = state
|
|
.storage
|
|
.get_watch_progress(user_id, MediaId(id))
|
|
.await?
|
|
.unwrap_or(0.0);
|
|
Ok(Json(WatchProgressResponse {
|
|
progress_secs: progress,
|
|
}))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/api/v1/media/{id}/progress",
|
|
tag = "analytics",
|
|
params(
|
|
("id" = Uuid, Path, description = "Media item ID"),
|
|
),
|
|
request_body = WatchProgressRequest,
|
|
responses(
|
|
(status = 200, description = "Progress updated"),
|
|
(status = 400, description = "Bad request"),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 404, description = "Not found"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn update_watch_progress(
|
|
State(state): State<AppState>,
|
|
Extension(username): Extension<String>,
|
|
Path(id): Path<Uuid>,
|
|
Json(req): Json<WatchProgressRequest>,
|
|
) -> Result<Json<serde_json::Value>, ApiError> {
|
|
if !req.progress_secs.is_finite() || req.progress_secs < 0.0 {
|
|
return Err(ApiError::bad_request(
|
|
"progress_secs must be a non-negative finite number",
|
|
));
|
|
}
|
|
let user_id = resolve_user_id(&state.storage, &username).await?;
|
|
state
|
|
.storage
|
|
.update_watch_progress(user_id, MediaId(id), req.progress_secs)
|
|
.await?;
|
|
Ok(Json(serde_json::json!({"updated": true})))
|
|
}
|