various: simplify code; work on security and performance

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
raf 2026-02-02 17:32:11 +03:00
commit c4adc4e3e0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
75 changed files with 12921 additions and 358 deletions

View file

@ -21,7 +21,9 @@ async fn test_media_crud() {
id,
path: "/tmp/test.txt".into(),
file_name: "test.txt".to_string(),
media_type: pinakes_core::media_type::MediaType::PlainText,
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::PlainText,
),
content_hash: ContentHash::new("abc123".to_string()),
file_size: 100,
title: Some("Test Title".to_string()),
@ -97,7 +99,9 @@ async fn test_tags() {
id,
path: "/tmp/song.mp3".into(),
file_name: "song.mp3".to_string(),
media_type: pinakes_core::media_type::MediaType::Mp3,
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::Mp3,
),
content_hash: ContentHash::new("hash1".to_string()),
file_size: 5000,
title: Some("Test Song".to_string()),
@ -147,7 +151,9 @@ async fn test_collections() {
id,
path: "/tmp/doc.pdf".into(),
file_name: "doc.pdf".to_string(),
media_type: pinakes_core::media_type::MediaType::Pdf,
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::Pdf,
),
content_hash: ContentHash::new("pdfhash".to_string()),
file_size: 10000,
title: None,
@ -192,7 +198,9 @@ async fn test_custom_fields() {
id,
path: "/tmp/test.md".into(),
file_name: "test.md".to_string(),
media_type: pinakes_core::media_type::MediaType::Markdown,
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::Markdown,
),
content_hash: ContentHash::new("mdhash".to_string()),
file_size: 500,
title: None,
@ -387,7 +395,9 @@ async fn test_library_statistics_with_data() {
id: MediaId::new(),
path: "/tmp/stats_test.mp3".into(),
file_name: "stats_test.mp3".to_string(),
media_type: pinakes_core::media_type::MediaType::Mp3,
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::Mp3,
),
content_hash: ContentHash::new("stats_hash".to_string()),
file_size: 5000,
title: Some("Stats Song".to_string()),
@ -412,3 +422,449 @@ async fn test_library_statistics_with_data() {
assert!(stats.newest_item.is_some());
assert!(stats.oldest_item.is_some());
}
// ===== Phase 2: Media Server Features =====
fn make_test_media(hash: &str) -> MediaItem {
let now = chrono::Utc::now();
MediaItem {
id: MediaId::new(),
path: format!("/tmp/test_{hash}.mp4").into(),
file_name: format!("test_{hash}.mp4"),
media_type: pinakes_core::media_type::MediaType::Builtin(
pinakes_core::media_type::BuiltinMediaType::Mp4,
),
content_hash: ContentHash::new(hash.to_string()),
file_size: 1000,
title: Some(format!("Test {hash}")),
artist: Some("Test Artist".to_string()),
album: None,
genre: None,
year: Some(2024),
duration_secs: Some(120.0),
description: None,
thumbnail_path: None,
custom_fields: HashMap::new(),
created_at: now,
updated_at: now,
}
}
#[tokio::test]
async fn test_ratings_crud() {
let storage = setup().await;
let item = make_test_media("rating1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
// Rate media
let rating = storage
.rate_media(user_id, item.id, 4, Some("Great video"))
.await
.unwrap();
assert_eq!(rating.stars, 4);
assert_eq!(rating.review_text.as_deref(), Some("Great video"));
// Get user's rating
let fetched = storage.get_user_rating(user_id, item.id).await.unwrap();
assert!(fetched.is_some());
assert_eq!(fetched.unwrap().stars, 4);
// Get media ratings
let ratings = storage.get_media_ratings(item.id).await.unwrap();
assert_eq!(ratings.len(), 1);
// Delete rating
storage.delete_rating(rating.id).await.unwrap();
let empty = storage.get_media_ratings(item.id).await.unwrap();
assert!(empty.is_empty());
}
#[tokio::test]
async fn test_comments_crud() {
let storage = setup().await;
let item = make_test_media("comment1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
// Add comment
let comment = storage
.add_comment(user_id, item.id, "Nice video!", None)
.await
.unwrap();
assert_eq!(comment.text, "Nice video!");
assert!(comment.parent_comment_id.is_none());
// Add reply
let reply = storage
.add_comment(user_id, item.id, "Thanks!", Some(comment.id))
.await
.unwrap();
assert_eq!(reply.parent_comment_id, Some(comment.id));
// List comments
let comments = storage.get_media_comments(item.id).await.unwrap();
assert_eq!(comments.len(), 2);
// Delete comment
storage.delete_comment(reply.id).await.unwrap();
let remaining = storage.get_media_comments(item.id).await.unwrap();
assert_eq!(remaining.len(), 1);
}
#[tokio::test]
async fn test_favorites_toggle() {
let storage = setup().await;
let item = make_test_media("fav1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
// Not a favorite initially
assert!(!storage.is_favorite(user_id, item.id).await.unwrap());
// Add favorite
storage.add_favorite(user_id, item.id).await.unwrap();
assert!(storage.is_favorite(user_id, item.id).await.unwrap());
// List favorites
let favs = storage
.get_user_favorites(user_id, &Pagination::default())
.await
.unwrap();
assert_eq!(favs.len(), 1);
// Remove favorite
storage.remove_favorite(user_id, item.id).await.unwrap();
assert!(!storage.is_favorite(user_id, item.id).await.unwrap());
}
#[tokio::test]
async fn test_share_links() {
let storage = setup().await;
let item = make_test_media("share1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
let token = "test_share_token_abc123";
// Create share link
let link = storage
.create_share_link(item.id, user_id, token, None, None)
.await
.unwrap();
assert_eq!(link.token, token);
assert_eq!(link.view_count, 0);
// Get share link
let fetched = storage.get_share_link(token).await.unwrap();
assert_eq!(fetched.media_id, item.id);
// Increment views
storage.increment_share_views(token).await.unwrap();
let updated = storage.get_share_link(token).await.unwrap();
assert_eq!(updated.view_count, 1);
// Delete share link
storage.delete_share_link(link.id).await.unwrap();
let result = storage.get_share_link(token).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_playlists_crud() {
let storage = setup().await;
let item1 = make_test_media("pl1");
let item2 = make_test_media("pl2");
storage.insert_media(&item1).await.unwrap();
storage.insert_media(&item2).await.unwrap();
let owner = pinakes_core::users::UserId::new();
// Create playlist
let playlist = storage
.create_playlist(
owner,
"My Playlist",
Some("A test playlist"),
true,
false,
None,
)
.await
.unwrap();
assert_eq!(playlist.name, "My Playlist");
assert!(playlist.is_public);
// Get playlist
let fetched = storage.get_playlist(playlist.id).await.unwrap();
assert_eq!(fetched.name, "My Playlist");
// Add items
storage
.add_to_playlist(playlist.id, item1.id, 0)
.await
.unwrap();
storage
.add_to_playlist(playlist.id, item2.id, 1)
.await
.unwrap();
// Get playlist items
let items = storage.get_playlist_items(playlist.id).await.unwrap();
assert_eq!(items.len(), 2);
// Reorder
storage
.reorder_playlist(playlist.id, item2.id, 0)
.await
.unwrap();
// Remove item
storage
.remove_from_playlist(playlist.id, item1.id)
.await
.unwrap();
let items = storage.get_playlist_items(playlist.id).await.unwrap();
assert_eq!(items.len(), 1);
// Update playlist
let updated = storage
.update_playlist(playlist.id, Some("Renamed"), None, Some(false))
.await
.unwrap();
assert_eq!(updated.name, "Renamed");
assert!(!updated.is_public);
// List playlists
let playlists = storage.list_playlists(None).await.unwrap();
assert!(!playlists.is_empty());
// Delete playlist
storage.delete_playlist(playlist.id).await.unwrap();
let result = storage.get_playlist(playlist.id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_analytics_usage_events() {
let storage = setup().await;
let item = make_test_media("analytics1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
// Record events
let event = pinakes_core::analytics::UsageEvent {
id: uuid::Uuid::now_v7(),
media_id: Some(item.id),
user_id: Some(user_id),
event_type: pinakes_core::analytics::UsageEventType::View,
timestamp: chrono::Utc::now(),
duration_secs: Some(60.0),
context_json: None,
};
storage.record_usage_event(&event).await.unwrap();
// Get usage events
let events = storage
.get_usage_events(Some(item.id), None, 10)
.await
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(
events[0].event_type,
pinakes_core::analytics::UsageEventType::View
);
// Most viewed
let most_viewed = storage.get_most_viewed(10).await.unwrap();
assert_eq!(most_viewed.len(), 1);
assert_eq!(most_viewed[0].1, 1);
// Recently viewed
let recent = storage.get_recently_viewed(user_id, 10).await.unwrap();
assert_eq!(recent.len(), 1);
}
#[tokio::test]
async fn test_watch_progress() {
let storage = setup().await;
let item = make_test_media("progress1");
storage.insert_media(&item).await.unwrap();
let user_id = pinakes_core::users::UserId::new();
// No progress initially
let progress = storage.get_watch_progress(user_id, item.id).await.unwrap();
assert!(progress.is_none());
// Update progress
storage
.update_watch_progress(user_id, item.id, 45.5)
.await
.unwrap();
let progress = storage.get_watch_progress(user_id, item.id).await.unwrap();
assert_eq!(progress, Some(45.5));
// Update again (should upsert)
storage
.update_watch_progress(user_id, item.id, 90.0)
.await
.unwrap();
let progress = storage.get_watch_progress(user_id, item.id).await.unwrap();
assert_eq!(progress, Some(90.0));
}
#[tokio::test]
async fn test_cleanup_old_events() {
let storage = setup().await;
let old_event = pinakes_core::analytics::UsageEvent {
id: uuid::Uuid::now_v7(),
media_id: None,
user_id: None,
event_type: pinakes_core::analytics::UsageEventType::Search,
timestamp: chrono::Utc::now() - chrono::Duration::days(100),
duration_secs: None,
context_json: None,
};
storage.record_usage_event(&old_event).await.unwrap();
let cutoff = chrono::Utc::now() - chrono::Duration::days(90);
let cleaned = storage.cleanup_old_events(cutoff).await.unwrap();
assert_eq!(cleaned, 1);
}
#[tokio::test]
async fn test_subtitles_crud() {
let storage = setup().await;
let item = make_test_media("sub1");
storage.insert_media(&item).await.unwrap();
let subtitle = pinakes_core::subtitles::Subtitle {
id: uuid::Uuid::now_v7(),
media_id: item.id,
language: Some("en".to_string()),
format: pinakes_core::subtitles::SubtitleFormat::Srt,
file_path: Some("/tmp/test.srt".into()),
is_embedded: false,
track_index: None,
offset_ms: 0,
created_at: chrono::Utc::now(),
};
storage.add_subtitle(&subtitle).await.unwrap();
// Get subtitles
let subs = storage.get_media_subtitles(item.id).await.unwrap();
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].language.as_deref(), Some("en"));
assert_eq!(subs[0].format, pinakes_core::subtitles::SubtitleFormat::Srt);
// Update offset
storage
.update_subtitle_offset(subtitle.id, 500)
.await
.unwrap();
let updated = storage.get_media_subtitles(item.id).await.unwrap();
assert_eq!(updated[0].offset_ms, 500);
// Delete subtitle
storage.delete_subtitle(subtitle.id).await.unwrap();
let empty = storage.get_media_subtitles(item.id).await.unwrap();
assert!(empty.is_empty());
}
#[tokio::test]
async fn test_external_metadata() {
let storage = setup().await;
let item = make_test_media("enrich1");
storage.insert_media(&item).await.unwrap();
let meta = pinakes_core::enrichment::ExternalMetadata {
id: uuid::Uuid::now_v7(),
media_id: item.id,
source: pinakes_core::enrichment::EnrichmentSourceType::MusicBrainz,
external_id: Some("mb-123".to_string()),
metadata_json: r#"{"title":"Test"}"#.to_string(),
confidence: 0.85,
last_updated: chrono::Utc::now(),
};
storage.store_external_metadata(&meta).await.unwrap();
// Get external metadata
let metas = storage.get_external_metadata(item.id).await.unwrap();
assert_eq!(metas.len(), 1);
assert_eq!(
metas[0].source,
pinakes_core::enrichment::EnrichmentSourceType::MusicBrainz
);
assert_eq!(metas[0].external_id.as_deref(), Some("mb-123"));
assert!((metas[0].confidence - 0.85).abs() < 0.01);
// Delete
storage.delete_external_metadata(meta.id).await.unwrap();
let empty = storage.get_external_metadata(item.id).await.unwrap();
assert!(empty.is_empty());
}
#[tokio::test]
async fn test_transcode_sessions() {
let storage = setup().await;
let item = make_test_media("transcode1");
storage.insert_media(&item).await.unwrap();
let session = pinakes_core::transcode::TranscodeSession {
id: uuid::Uuid::now_v7(),
media_id: item.id,
user_id: None,
profile: "720p".to_string(),
cache_path: "/tmp/transcode/test.mp4".into(),
status: pinakes_core::transcode::TranscodeStatus::Pending,
progress: 0.0,
created_at: chrono::Utc::now(),
expires_at: Some(chrono::Utc::now() + chrono::Duration::hours(24)),
duration_secs: None,
child_cancel: None,
};
storage.create_transcode_session(&session).await.unwrap();
// Get session
let fetched = storage.get_transcode_session(session.id).await.unwrap();
assert_eq!(fetched.profile, "720p");
assert_eq!(fetched.status.as_str(), "pending");
// Update status
storage
.update_transcode_status(
session.id,
pinakes_core::transcode::TranscodeStatus::Transcoding,
0.5,
)
.await
.unwrap();
let updated = storage.get_transcode_session(session.id).await.unwrap();
assert_eq!(updated.status.as_str(), "transcoding");
assert!((updated.progress - 0.5).abs() < 0.01);
// List sessions
let sessions = storage.list_transcode_sessions(None).await.unwrap();
assert_eq!(sessions.len(), 1);
// List by media ID
let sessions = storage
.list_transcode_sessions(Some(item.id))
.await
.unwrap();
assert_eq!(sessions.len(), 1);
// Cleanup expired
let far_future = chrono::Utc::now() + chrono::Duration::days(365);
let cleaned = storage
.cleanup_expired_transcodes(far_future)
.await
.unwrap();
assert_eq!(cleaned, 1);
}