224 lines
9.1 KiB
Rust
224 lines
9.1 KiB
Rust
mod config;
|
||
mod models;
|
||
mod db;
|
||
mod engine;
|
||
mod cache;
|
||
mod routes;
|
||
mod services;
|
||
mod image_processor;
|
||
mod queue;
|
||
|
||
use crate::cache::AlbumsCache;
|
||
use actix_files as afs;
|
||
use actix_web::{App, HttpResponse, HttpServer, middleware, web};
|
||
use std::fs;
|
||
use std::sync::Arc;
|
||
use tokio::time::{Duration, interval};
|
||
use tracing::{error, info};
|
||
|
||
#[global_allocator]
|
||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||
|
||
#[actix_web::main]
|
||
async fn main() -> std::io::Result<()> {
|
||
tracing_subscriber::fmt::init();
|
||
|
||
// 加载配置
|
||
let config = match config::Config::load() {
|
||
Ok(cfg) => cfg,
|
||
Err(e) => {
|
||
error!("加载配置失败: {}", e);
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
|
||
info!("启动图片展示服务 v2.0...");
|
||
info!("服务地址: http://{}", config.server_address());
|
||
|
||
// 初始化数据库
|
||
let db = match db::Database::new(&config).await {
|
||
Ok(db) => {
|
||
info!("数据库连接成功");
|
||
Arc::new(db)
|
||
}
|
||
Err(e) => {
|
||
error!("数据库连接失败: {}", e);
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
|
||
let image_processor = Arc::new(image_processor::ImageProcessor::new(config.storage.clone()));
|
||
|
||
// 后台导入本地相册 + 自动解析title
|
||
let config_clone = config.clone();
|
||
let db_clone = Arc::clone(&db);
|
||
let auto_parse = true;
|
||
tokio::spawn(async move {
|
||
info!("[后台任务] 开始扫描图片目录并导入新相册...");
|
||
let import_ok = match services::import::import_existing_data(&config_clone, Arc::clone(&db_clone), None).await {
|
||
Ok(_) => {
|
||
info!("[后台任务] 相册导入完成");
|
||
true
|
||
}
|
||
Err(e) => {
|
||
error!("[后台任务] 相册导入失败: {}", e);
|
||
false
|
||
}
|
||
};
|
||
|
||
if import_ok && auto_parse {
|
||
info!("[后台任务] 开始解析相册 title(提取人名 + 标签)...");
|
||
let (persons, tags) = engine::pipeline::process_all_albums(&db_clone).await;
|
||
info!("[后台任务] Title 解析完成:{} 个人名,{} 个标签", persons, tags);
|
||
}
|
||
});
|
||
|
||
// 相册内存缓存
|
||
let albums_cache = Arc::new(tokio::sync::RwLock::new(AlbumsCache::new()));
|
||
|
||
// 初始加载缓存
|
||
let initial_cache = Arc::clone(&albums_cache);
|
||
let initial_db = Arc::clone(&db);
|
||
tokio::spawn(async move {
|
||
match initial_db.get_all_albums().await {
|
||
Ok(albums) => {
|
||
let mut cache = initial_cache.write().await;
|
||
cache.update(albums);
|
||
println!("✅ 初始相册缓存加载完成");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("❌ 初始缓存加载失败: {}", e);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 定时刷新缓存(每30分钟)
|
||
let refresh_cache = Arc::clone(&albums_cache);
|
||
let refresh_db = Arc::clone(&db);
|
||
tokio::spawn(async move {
|
||
let mut interval = interval(Duration::from_secs(30 * 60));
|
||
loop {
|
||
interval.tick().await;
|
||
match refresh_db.get_all_albums().await {
|
||
Ok(albums) => {
|
||
let mut cache = refresh_cache.write().await;
|
||
cache.update(albums);
|
||
println!("🔄 相册缓存定时刷新完成");
|
||
}
|
||
Err(e) => {
|
||
eprintln!("❌ 缓存刷新失败: {}", e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 任务队列
|
||
let task_queue = Arc::new(queue::TaskQueue::new(
|
||
config.queue.max_concurrent_tasks,
|
||
config.queue.queue_size_limit,
|
||
config.queue.high_request_rate_threshold,
|
||
config.queue.idle_request_rate_threshold,
|
||
));
|
||
|
||
println!("API接口:");
|
||
println!(" - GET /api/albums - 获取所有相册");
|
||
println!(" - GET /api/search?q=关键词 - 搜索相册");
|
||
println!(" - GET /api/album/random - 随机相册");
|
||
println!(" - GET /api/album/:id - 获取相册详情");
|
||
println!(" - POST /api/albums - 创建新相册");
|
||
println!(" - GET /api/image/:id - 获取图片");
|
||
println!(" - GET /api/favorites - 收藏列表");
|
||
println!(" - POST /api/favorites - 添加收藏");
|
||
println!(" - DELETE /api/favorites/:id - 取消收藏");
|
||
println!("元数据接口:");
|
||
println!(" - GET /api/people - 人员列表");
|
||
println!(" - GET /api/people/search?q=关键词 - 搜索人员");
|
||
println!(" - GET /api/people/{{name}} - 人员详情");
|
||
println!(" - GET /api/people/{{id}}/aliases - 人员别名");
|
||
println!(" - GET /api/metadata/stats - 元数据统计");
|
||
println!(" - GET /api/tags/search?tag=标签 - 按标签搜索");
|
||
println!(" - GET /api/tags/popular?limit=50 - 热门标签");
|
||
println!("推荐接口:");
|
||
println!(" - GET /api/recommend/tags?q=xxx - 标签推荐");
|
||
println!(" - GET /api/recommend/persons?q=xxx - 人物推荐");
|
||
println!(" - GET /api/recommend/related?context=xxx - 关联推荐");
|
||
println!(" - GET /api/recommend/popular-persons - 热门人物");
|
||
println!("迁移接口:");
|
||
println!(" - GET /api/migration/stats - 迁移状态");
|
||
println!(" - POST /api/migration/run?path=persons.db - 执行迁移");
|
||
println!(" - GET /api/queue/stats - 队列状态");
|
||
println!("按 Ctrl+C 停止服务");
|
||
|
||
fs::create_dir_all(&config.storage.images_dir).unwrap_or(());
|
||
|
||
HttpServer::new(move || {
|
||
App::new()
|
||
.wrap(middleware::Compress::default())
|
||
.wrap(middleware::Logger::default())
|
||
.app_data(web::Data::new(Arc::clone(&db)))
|
||
.app_data(web::Data::new(Arc::clone(&image_processor)))
|
||
.app_data(web::Data::new(Arc::clone(&albums_cache)))
|
||
.app_data(web::Data::new(Arc::clone(&task_queue)))
|
||
// 相册 API
|
||
.route("/api/albums", web::get().to(routes::get_albums))
|
||
.route("/api/search", web::get().to(routes::search_albums))
|
||
.route("/api/album/random", web::get().to(routes::get_random_album))
|
||
.route("/api/album/{id}", web::get().to(routes::get_album_detail))
|
||
.route("/api/albums", web::post().to(routes::create_album))
|
||
.route("/api/image/{id}", web::get().to(routes::get_image))
|
||
// 收藏 API
|
||
.route("/api/favorites", web::get().to(routes::get_favorites))
|
||
.route("/api/favorites/status/{id}", web::get().to(routes::check_favorite_status))
|
||
.route("/api/favorites", web::post().to(routes::add_favorite))
|
||
.route("/api/favorites/{id}", web::delete().to(routes::remove_favorite))
|
||
.route("/api/favorites/image/{id}", web::delete().to(routes::remove_favorite_by_image))
|
||
// 人员 API
|
||
.route("/api/people", web::get().to(routes::get_people))
|
||
.route("/api/people/search", web::get().to(routes::search_people))
|
||
.route("/api/people/{name}", web::get().to(routes::get_person_detail))
|
||
.route("/api/people/{id}/aliases", web::get().to(routes::get_person_aliases))
|
||
// 标签 API
|
||
.route("/api/metadata/stats", web::get().to(routes::get_metadata_stats))
|
||
.route("/api/tags/search", web::get().to(routes::search_by_tag))
|
||
.route("/api/tags/popular", web::get().to(routes::get_popular_tags))
|
||
// 推荐 API
|
||
.route("/api/recommend/tags", web::get().to(routes::recommend_tags))
|
||
.route("/api/recommend/persons", web::get().to(routes::recommend_persons))
|
||
.route("/api/recommend/related", web::get().to(routes::recommend_related))
|
||
.route("/api/recommend/popular-persons", web::get().to(routes::popular_persons))
|
||
// 迁移 API
|
||
.route("/api/migration/stats", web::get().to(routes::migration_stats))
|
||
.route("/api/migration/run", web::post().to(routes::migration_run))
|
||
// 管线 API
|
||
.route("/api/pipeline/run", web::post().to(routes::pipeline_run))
|
||
// 队列 API
|
||
.route("/api/queue/stats", web::get().to(routes::get_queue_stats))
|
||
// 静态文件
|
||
.service(
|
||
afs::Files::new("/assets", "./assets")
|
||
.show_files_listing()
|
||
.prefer_utf8(true)
|
||
.use_last_modified(true)
|
||
.use_etag(true),
|
||
)
|
||
.route("/", web::get().to(|| async {
|
||
match fs::read_to_string("./assets/index.html") {
|
||
Ok(content) => HttpResponse::Ok()
|
||
.content_type("text/html; charset=utf-8")
|
||
.body(content),
|
||
Err(_) => HttpResponse::NotFound().body("首页文件不存在"),
|
||
}
|
||
}))
|
||
.route("/album", web::get().to(|| async {
|
||
match fs::read_to_string("./assets/album.html") {
|
||
Ok(content) => HttpResponse::Ok()
|
||
.content_type("text/html; charset=utf-8")
|
||
.body(content),
|
||
Err(_) => HttpResponse::NotFound().body("相册页面不存在"),
|
||
}
|
||
}))
|
||
})
|
||
.bind(config.server_address())?
|
||
.run()
|
||
.await
|
||
} |