mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-07-12 16:11:32 +08:00
feat: 支持 webui 加载用户的订阅与收藏,一键点击订阅 (#357)
This commit is contained in:
@@ -2,29 +2,45 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Extension, Path, Query};
|
||||
use axum::response::Response;
|
||||
use axum::routing::{get, post};
|
||||
use bili_sync_entity::*;
|
||||
use bili_sync_migration::Expr;
|
||||
use bili_sync_migration::{Expr, OnConflict};
|
||||
use reqwest::{Method, StatusCode, header};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{
|
||||
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
|
||||
TransactionTrait,
|
||||
};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use super::request::ImageProxyParams;
|
||||
use crate::api::auth::OpenAPIAuth;
|
||||
use crate::api::error::InnerApiError;
|
||||
use crate::api::helper::{update_page_download_status, update_video_download_status};
|
||||
use crate::api::request::{UpdateVideoStatusRequest, VideosRequest};
|
||||
use crate::api::request::{
|
||||
FollowedCollectionsRequest, FollowedUppersRequest, UpdateVideoStatusRequest, UpsertCollectionRequest,
|
||||
UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest,
|
||||
};
|
||||
use crate::api::response::{
|
||||
PageInfo, ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, VideoInfo, VideoResponse,
|
||||
VideoSource, VideoSourcesResponse, VideosResponse,
|
||||
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, PageInfo,
|
||||
ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, UpperWithSubscriptionStatus, UppersResponse,
|
||||
VideoInfo, VideoResponse, VideoSource, VideoSourcesResponse, VideosResponse,
|
||||
};
|
||||
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Me, Submission};
|
||||
use crate::utils::status::{PageStatus, VideoStatus};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(get_video_sources, get_videos, get_video, reset_video, reset_all_videos, update_video_status),
|
||||
paths(
|
||||
get_video_sources, get_videos, get_video, reset_video, reset_all_videos, update_video_status,
|
||||
get_created_favorites, get_followed_collections, get_followed_uppers,
|
||||
upsert_favorite, upsert_collection, upsert_submission
|
||||
),
|
||||
modifiers(&OpenAPIAuth),
|
||||
security(
|
||||
("Token" = []),
|
||||
@@ -32,6 +48,23 @@ use crate::utils::status::{PageStatus, VideoStatus};
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
pub fn api_router() -> Router {
|
||||
Router::new()
|
||||
.route("/api/video-sources", get(get_video_sources))
|
||||
.route("/api/video-sources/collections", post(upsert_collection))
|
||||
.route("/api/video-sources/favorites", post(upsert_favorite))
|
||||
.route("/api/video-sources/submissions", post(upsert_submission))
|
||||
.route("/api/videos", get(get_videos))
|
||||
.route("/api/videos/{id}", get(get_video))
|
||||
.route("/api/videos/{id}/reset", post(reset_video))
|
||||
.route("/api/videos/reset-all", post(reset_all_videos))
|
||||
.route("/api/videos/{id}/update-status", post(update_video_status))
|
||||
.route("/api/me/favorites", get(get_created_favorites))
|
||||
.route("/api/me/collections", get(get_followed_collections))
|
||||
.route("/api/me/uppers", get(get_followed_uppers))
|
||||
.route("/image-proxy", get(image_proxy))
|
||||
}
|
||||
|
||||
/// 列出所有视频来源
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -287,7 +320,7 @@ pub async fn reset_all_videos(
|
||||
/// 更新特定视频及其所含分页的状态位
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/videos/{id}/update-status",
|
||||
path = "/api/video/{id}/update-status",
|
||||
request_body = UpdateVideoStatusRequest,
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<UpdateVideoStatusResponse>),
|
||||
@@ -349,3 +382,299 @@ pub async fn update_video_status(
|
||||
pages: pages_info,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 获取当前用户创建的收藏夹列表,包含订阅状态
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/me/favorites",
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<FavoritesResponse>),
|
||||
)
|
||||
)]
|
||||
pub async fn get_created_favorites(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
) -> Result<ApiResponse<FavoritesResponse>, ApiError> {
|
||||
let me = Me::new(bili_client.as_ref());
|
||||
let bili_favorites = me.get_created_favorites().await?;
|
||||
|
||||
let favorites = if let Some(bili_favorites) = bili_favorites {
|
||||
// b 站收藏夹相关接口使用的所谓 “fid” 其实是该处的 id,即 fid + mid 后两位
|
||||
let bili_fids: Vec<_> = bili_favorites.iter().map(|fav| fav.id).collect();
|
||||
|
||||
let subscribed_fids: Vec<i64> = favorite::Entity::find()
|
||||
.select_only()
|
||||
.column(favorite::Column::FId)
|
||||
.filter(favorite::Column::FId.is_in(bili_fids))
|
||||
.into_tuple()
|
||||
.all(db.as_ref())
|
||||
.await?;
|
||||
let subscribed_set: HashSet<i64> = subscribed_fids.into_iter().collect();
|
||||
|
||||
bili_favorites
|
||||
.into_iter()
|
||||
.map(|fav| FavoriteWithSubscriptionStatus {
|
||||
title: fav.title,
|
||||
media_count: fav.media_count,
|
||||
fid: fav.fid,
|
||||
mid: fav.mid,
|
||||
subscribed: subscribed_set.contains(&fav.id),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
|
||||
}
|
||||
|
||||
/// 获取当前用户关注的合集列表,包含订阅状态
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/me/collections",
|
||||
params(
|
||||
FollowedCollectionsRequest,
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<CollectionsResponse>),
|
||||
)
|
||||
)]
|
||||
pub async fn get_followed_collections(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
Query(params): Query<FollowedCollectionsRequest>,
|
||||
) -> Result<ApiResponse<CollectionsResponse>, ApiError> {
|
||||
let me = Me::new(bili_client.as_ref());
|
||||
let (page_num, page_size) = (params.page_num.unwrap_or(1), params.page_size.unwrap_or(50));
|
||||
let bili_collections = me.get_followed_collections(page_num, page_size).await?;
|
||||
|
||||
let collections = if let Some(collection_list) = &bili_collections.list {
|
||||
let bili_sids: Vec<_> = collection_list.iter().map(|col| col.id).collect();
|
||||
|
||||
let subscribed_ids: Vec<i64> = collection::Entity::find()
|
||||
.select_only()
|
||||
.column(collection::Column::SId)
|
||||
.filter(collection::Column::SId.is_in(bili_sids))
|
||||
.into_tuple()
|
||||
.all(db.as_ref())
|
||||
.await?;
|
||||
let subscribed_set: HashSet<i64> = subscribed_ids.into_iter().collect();
|
||||
|
||||
collection_list
|
||||
.iter()
|
||||
.map(|col| CollectionWithSubscriptionStatus {
|
||||
id: col.id,
|
||||
mid: col.mid,
|
||||
state: col.state,
|
||||
title: col.title.clone(),
|
||||
subscribed: subscribed_set.contains(&col.id),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(ApiResponse::ok(CollectionsResponse {
|
||||
collections,
|
||||
total: bili_collections.count,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/me/uppers",
|
||||
params(
|
||||
FollowedUppersRequest,
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<UppersResponse>),
|
||||
)
|
||||
)]
|
||||
pub async fn get_followed_uppers(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
Query(params): Query<FollowedUppersRequest>,
|
||||
) -> Result<ApiResponse<UppersResponse>, ApiError> {
|
||||
let me = Me::new(bili_client.as_ref());
|
||||
let (page_num, page_size) = (params.page_num.unwrap_or(1), params.page_size.unwrap_or(20));
|
||||
let bili_uppers = me.get_followed_uppers(page_num, page_size).await?;
|
||||
|
||||
let bili_uid: Vec<_> = bili_uppers.list.iter().map(|upper| upper.mid).collect();
|
||||
|
||||
let subscribed_ids: Vec<i64> = submission::Entity::find()
|
||||
.select_only()
|
||||
.column(submission::Column::UpperId)
|
||||
.filter(submission::Column::UpperId.is_in(bili_uid))
|
||||
.into_tuple()
|
||||
.all(db.as_ref())
|
||||
.await?;
|
||||
let subscribed_set: HashSet<i64> = subscribed_ids.into_iter().collect();
|
||||
|
||||
let uppers = bili_uppers
|
||||
.list
|
||||
.into_iter()
|
||||
.map(|upper| UpperWithSubscriptionStatus {
|
||||
mid: upper.mid,
|
||||
uname: upper.uname,
|
||||
face: upper.face,
|
||||
sign: upper.sign,
|
||||
subscribed: subscribed_set.contains(&upper.mid),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ApiResponse::ok(UppersResponse {
|
||||
uppers,
|
||||
total: bili_uppers.total,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/video-sources/favorites",
|
||||
request_body = UpsertFavoriteRequest,
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<bool>),
|
||||
)
|
||||
)]
|
||||
pub async fn upsert_favorite(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
ValidatedJson(request): ValidatedJson<UpsertFavoriteRequest>,
|
||||
) -> Result<ApiResponse<bool>, ApiError> {
|
||||
let favorite = FavoriteList::new(bili_client.as_ref(), request.fid.to_string());
|
||||
let favorite_info = favorite.get_info().await?;
|
||||
favorite::Entity::insert(favorite::ActiveModel {
|
||||
f_id: Set(favorite_info.id),
|
||||
name: Set(favorite_info.title.clone()),
|
||||
path: Set(request.path),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(favorite::Column::FId)
|
||||
.update_columns([favorite::Column::Name, favorite::Column::Path])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::ok(true))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/video-sources/collections",
|
||||
request_body = UpsertCollectionRequest,
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<bool>),
|
||||
)
|
||||
)]
|
||||
pub async fn upsert_collection(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
ValidatedJson(request): ValidatedJson<UpsertCollectionRequest>,
|
||||
) -> Result<ApiResponse<bool>, ApiError> {
|
||||
let collection_item = CollectionItem {
|
||||
sid: request.id.to_string(),
|
||||
mid: request.mid.to_string(),
|
||||
collection_type: request.collection_type,
|
||||
};
|
||||
|
||||
let collection = Collection::new(bili_client.as_ref(), &collection_item);
|
||||
let collection_info = collection.get_info().await?;
|
||||
|
||||
collection::Entity::insert(collection::ActiveModel {
|
||||
s_id: Set(collection_info.sid),
|
||||
m_id: Set(collection_info.mid),
|
||||
r#type: Set(collection_info.collection_type.into()),
|
||||
name: Set(collection_info.name.clone()),
|
||||
path: Set(request.path),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
collection::Column::SId,
|
||||
collection::Column::MId,
|
||||
collection::Column::Type,
|
||||
])
|
||||
.update_columns([collection::Column::Name, collection::Column::Path])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::ok(true))
|
||||
}
|
||||
|
||||
/// 订阅UP主投稿
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/video-sources/submissions",
|
||||
request_body = UpsertSubmissionRequest,
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<bool>),
|
||||
)
|
||||
)]
|
||||
pub async fn upsert_submission(
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
ValidatedJson(request): ValidatedJson<UpsertSubmissionRequest>,
|
||||
) -> Result<ApiResponse<bool>, ApiError> {
|
||||
let submission = Submission::new(bili_client.as_ref(), request.upper_id.to_string());
|
||||
let upper = submission.get_info().await?;
|
||||
|
||||
submission::Entity::insert(submission::ActiveModel {
|
||||
upper_id: Set(upper.mid.parse()?),
|
||||
upper_name: Set(upper.name),
|
||||
path: Set(request.path),
|
||||
..Default::default()
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::column(submission::Column::UpperId)
|
||||
.update_columns([submission::Column::UpperName, submission::Column::Path])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(db.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::ok(true))
|
||||
}
|
||||
|
||||
/// B 站的图片会检查 referer,需要做个转发伪造一下,否则直接返回 403
|
||||
pub async fn image_proxy(
|
||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||
Query(params): Query<ImageProxyParams>,
|
||||
) -> Response {
|
||||
let resp = bili_client.client.request(Method::GET, ¶ms.url, None).send().await;
|
||||
let whitelist = [
|
||||
header::CONTENT_TYPE,
|
||||
header::CONTENT_LENGTH,
|
||||
header::CACHE_CONTROL,
|
||||
header::EXPIRES,
|
||||
header::LAST_MODIFIED,
|
||||
header::ETAG,
|
||||
header::CONTENT_DISPOSITION,
|
||||
header::CONTENT_ENCODING,
|
||||
header::ACCEPT_RANGES,
|
||||
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let builder = Response::builder();
|
||||
|
||||
let response = match resp {
|
||||
Err(e) => builder.status(StatusCode::BAD_GATEWAY).body(Body::new(e.to_string())),
|
||||
Ok(res) => {
|
||||
let mut response = builder.status(res.status());
|
||||
for (k, v) in res.headers() {
|
||||
if whitelist.contains(k) {
|
||||
response = response.header(k, v);
|
||||
}
|
||||
}
|
||||
let streams = res.bytes_stream();
|
||||
response.body(Body::from_stream(streams))
|
||||
}
|
||||
};
|
||||
//safety: all previously configured headers are taken from a valid response, ensuring the response is safe to use
|
||||
response.unwrap()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use validator::Validate;
|
||||
|
||||
use crate::bilibili::CollectionType;
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct VideosRequest {
|
||||
pub collection: Option<i32>,
|
||||
@@ -36,3 +38,45 @@ pub struct UpdateVideoStatusRequest {
|
||||
#[validate(nested)]
|
||||
pub page_updates: Vec<PageStatusUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct FollowedCollectionsRequest {
|
||||
pub page_num: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct FollowedUppersRequest {
|
||||
pub page_num: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, Validate)]
|
||||
pub struct UpsertFavoriteRequest {
|
||||
pub fid: i64,
|
||||
#[validate(custom(function = "crate::utils::validation::validate_path"))]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, Validate)]
|
||||
pub struct UpsertCollectionRequest {
|
||||
pub id: i64,
|
||||
pub mid: i64,
|
||||
#[schema(value_type = i8)]
|
||||
#[serde(default)]
|
||||
pub collection_type: CollectionType,
|
||||
#[validate(custom(function = "crate::utils::validation::validate_path"))]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, Validate)]
|
||||
pub struct UpsertSubmissionRequest {
|
||||
pub upper_id: i64,
|
||||
#[validate(custom(function = "crate::utils::validation::validate_path"))]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ImageProxyParams {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
@@ -90,3 +90,47 @@ where
|
||||
let status: [u32; 5] = PageStatus::from(*status).into();
|
||||
status.serialize(serializer)
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FavoriteWithSubscriptionStatus {
|
||||
pub title: String,
|
||||
pub media_count: i64,
|
||||
pub fid: i64,
|
||||
pub mid: i64,
|
||||
pub subscribed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CollectionWithSubscriptionStatus {
|
||||
pub id: i64,
|
||||
pub mid: i64,
|
||||
pub state: i32,
|
||||
pub title: String,
|
||||
pub subscribed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UpperWithSubscriptionStatus {
|
||||
pub mid: i64,
|
||||
pub uname: String,
|
||||
pub face: String,
|
||||
pub sign: String,
|
||||
pub subscribed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct FavoritesResponse {
|
||||
pub favorites: Vec<FavoriteWithSubscriptionStatus>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CollectionsResponse {
|
||||
pub collections: Vec<CollectionWithSubscriptionStatus>,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UppersResponse {
|
||||
pub uppers: Vec<UpperWithSubscriptionStatus>,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ use serde_json::Value;
|
||||
use crate::bilibili::credential::encoded_query;
|
||||
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo};
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug, Deserialize, Default)]
|
||||
pub enum CollectionType {
|
||||
Series,
|
||||
#[default]
|
||||
Season,
|
||||
}
|
||||
|
||||
|
||||
@@ -33,15 +33,15 @@ impl<'a> Me<'a> {
|
||||
Ok(serde_json::from_value(resp["data"]["list"].take())?)
|
||||
}
|
||||
|
||||
pub async fn get_followed_collections(&self, page: i32) -> Result<Collections> {
|
||||
pub async fn get_followed_collections(&self, page_num: i32, page_size: i32) -> Result<Collections> {
|
||||
let mut resp = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/v3/fav/folder/collected/list")
|
||||
.await
|
||||
.query(&[
|
||||
("up_mid", self.mid.as_ref()),
|
||||
("pn", page.to_string().as_ref()),
|
||||
("ps", "20"),
|
||||
("up_mid", self.mid.as_str()),
|
||||
("pn", page_num.to_string().as_str()),
|
||||
("ps", page_size.to_string().as_str()),
|
||||
("platform", "web"),
|
||||
])
|
||||
.send()
|
||||
@@ -53,15 +53,15 @@ impl<'a> Me<'a> {
|
||||
Ok(serde_json::from_value(resp["data"].take())?)
|
||||
}
|
||||
|
||||
pub async fn get_followed_uppers(&self, page: i32) -> Result<FollowedUppers> {
|
||||
pub async fn get_followed_uppers(&self, page_num: i32, page_size: i32) -> Result<FollowedUppers> {
|
||||
let mut resp = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/relation/followings")
|
||||
.await
|
||||
.query(&[
|
||||
("vmid", self.mid.as_ref()),
|
||||
("pn", page.to_string().as_ref()),
|
||||
("ps", "20"),
|
||||
("vmid", self.mid.as_str()),
|
||||
("pn", page_num.to_string().as_str()),
|
||||
("ps", page_size.to_string().as_str()),
|
||||
])
|
||||
.send()
|
||||
.await?
|
||||
@@ -86,6 +86,7 @@ impl<'a> Me<'a> {
|
||||
pub struct FavoriteItem {
|
||||
pub title: String,
|
||||
pub media_count: i64,
|
||||
pub id: i64,
|
||||
pub fid: i64,
|
||||
pub mid: i64,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use danmaku::DanmakuOption;
|
||||
pub use error::BiliError;
|
||||
pub use favorite_list::FavoriteList;
|
||||
use favorite_list::Upper;
|
||||
pub use me::Me;
|
||||
use once_cell::sync::Lazy;
|
||||
pub use submission::Submission;
|
||||
pub use video::{Dimension, PageInfo, Video};
|
||||
|
||||
@@ -16,6 +16,7 @@ use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bilibili::BiliClient;
|
||||
use once_cell::sync::Lazy;
|
||||
use task::{http_server, video_downloader};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -29,12 +30,26 @@ use crate::utils::signal::terminate;
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
init();
|
||||
let bili_client = Arc::new(BiliClient::new());
|
||||
let connection = Arc::new(setup_database().await);
|
||||
|
||||
let token = CancellationToken::new();
|
||||
let tracker = TaskTracker::new();
|
||||
|
||||
spawn_task("HTTP 服务", http_server(connection.clone()), &tracker, token.clone());
|
||||
spawn_task("定时下载", video_downloader(connection), &tracker, token.clone());
|
||||
spawn_task(
|
||||
"HTTP 服务",
|
||||
http_server(connection.clone(), bili_client.clone()),
|
||||
&tracker,
|
||||
token.clone(),
|
||||
);
|
||||
if !cfg!(debug_assertions) {
|
||||
spawn_task(
|
||||
"定时下载",
|
||||
video_downloader(connection, bili_client),
|
||||
&tracker,
|
||||
token.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
tracker.close();
|
||||
handle_shutdown(tracker, token).await
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::body::Body;
|
||||
use axum::extract::Request;
|
||||
use axum::http::{Response, Uri, header};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::routing::get;
|
||||
use axum::{Extension, Router, ServiceExt, middleware};
|
||||
use reqwest::StatusCode;
|
||||
use rust_embed_for_web::{EmbedableFile, RustEmbed};
|
||||
@@ -14,9 +14,8 @@ use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||
|
||||
use crate::api::auth;
|
||||
use crate::api::handler::{
|
||||
ApiDoc, get_video, get_video_sources, get_videos, reset_all_videos, reset_video, update_video_status,
|
||||
};
|
||||
use crate::api::handler::{ApiDoc, api_router};
|
||||
use crate::bilibili::BiliClient;
|
||||
use crate::config::CONFIG;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@@ -25,14 +24,9 @@ use crate::config::CONFIG;
|
||||
#[folder = "../../web/build"]
|
||||
struct Asset;
|
||||
|
||||
pub async fn http_server(database_connection: Arc<DatabaseConnection>) -> Result<()> {
|
||||
pub async fn http_server(database_connection: Arc<DatabaseConnection>, bili_client: Arc<BiliClient>) -> Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/api/video-sources", get(get_video_sources))
|
||||
.route("/api/videos", get(get_videos))
|
||||
.route("/api/videos/{id}", get(get_video))
|
||||
.route("/api/videos/{id}/reset", post(reset_video))
|
||||
.route("/api/videos/{id}/update-status", post(update_video_status))
|
||||
.route("/api/videos/reset-all", post(reset_all_videos))
|
||||
.merge(api_router())
|
||||
.merge(
|
||||
SwaggerUi::new("/swagger-ui/")
|
||||
.url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||
@@ -45,6 +39,7 @@ pub async fn http_server(database_connection: Arc<DatabaseConnection>) -> Result
|
||||
)
|
||||
.fallback_service(get(frontend_files))
|
||||
.layer(Extension(database_connection))
|
||||
.layer(Extension(bili_client))
|
||||
.layer(middleware::from_fn(auth::auth));
|
||||
let listener = tokio::net::TcpListener::bind(&CONFIG.bind_address)
|
||||
.await
|
||||
|
||||
@@ -8,9 +8,8 @@ use crate::config::CONFIG;
|
||||
use crate::workflow::process_video_source;
|
||||
|
||||
/// 启动周期下载视频的任务
|
||||
pub async fn video_downloader(connection: Arc<DatabaseConnection>) {
|
||||
pub async fn video_downloader(connection: Arc<DatabaseConnection>, bili_client: Arc<BiliClient>) {
|
||||
let mut anchor = chrono::Local::now().date_naive();
|
||||
let bili_client = BiliClient::new();
|
||||
let video_sources = CONFIG.as_video_sources();
|
||||
loop {
|
||||
info!("开始执行本轮视频下载任务..");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
use validator::ValidationError;
|
||||
|
||||
use crate::utils::status::{STATUS_NOT_STARTED, STATUS_OK};
|
||||
@@ -11,3 +13,11 @@ pub fn validate_status_value(value: u32) -> Result<(), ValidationError> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_path(path: &str) -> Result<(), ValidationError> {
|
||||
if path.is_empty() || !Path::new(path).is_absolute() {
|
||||
Err(ValidationError::new("path must be a non-empty absolute path"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user