mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 07:28:04 +08:00
refactor: 根据路径分割 api,避免单文件内容过多 (#376)
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
use axum::extract::Request;
|
|
||||||
use axum::http::HeaderMap;
|
|
||||||
use axum::middleware::Next;
|
|
||||||
use axum::response::{IntoResponse, Response};
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
|
|
||||||
use crate::api::wrapper::ApiResponse;
|
|
||||||
use crate::config::VersionedConfig;
|
|
||||||
|
|
||||||
pub async fn auth(headers: HeaderMap, request: Request, next: Next) -> Result<Response, StatusCode> {
|
|
||||||
if request.uri().path().starts_with("/api/")
|
|
||||||
&& get_token(&headers).is_none_or(|token| token != VersionedConfig::get().load().auth_token)
|
|
||||||
{
|
|
||||||
return Ok(ApiResponse::<()>::unauthorized("auth token does not match").into_response());
|
|
||||||
}
|
|
||||||
Ok(next.run(request).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_token(headers: &HeaderMap) -> Option<String> {
|
|
||||||
headers
|
|
||||||
.get("Authorization")
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
@@ -1,691 +0,0 @@
|
|||||||
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, put};
|
|
||||||
use bili_sync_entity::*;
|
|
||||||
use bili_sync_migration::Expr;
|
|
||||||
use reqwest::{Method, StatusCode, header};
|
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::{
|
|
||||||
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
|
|
||||||
TransactionTrait,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::request::ImageProxyParams;
|
|
||||||
use crate::adapter::_ActiveModel;
|
|
||||||
use crate::api::error::InnerApiError;
|
|
||||||
use crate::api::helper::{update_page_download_status, update_video_download_status};
|
|
||||||
use crate::api::request::{
|
|
||||||
FollowedCollectionsRequest, FollowedUppersRequest, InsertCollectionRequest, InsertFavoriteRequest,
|
|
||||||
InsertSubmissionRequest, UpdateVideoSourceRequest, UpdateVideoStatusRequest, VideosRequest,
|
|
||||||
};
|
|
||||||
use crate::api::response::{
|
|
||||||
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, PageInfo,
|
|
||||||
ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, UpperWithSubscriptionStatus, UppersResponse,
|
|
||||||
VideoInfo, VideoResponse, VideoSource, VideoSourceDetail, VideoSourcesDetailsResponse, VideoSourcesResponse,
|
|
||||||
VideosResponse,
|
|
||||||
};
|
|
||||||
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
|
||||||
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Me, Submission};
|
|
||||||
use crate::config::{Config, VersionedConfig};
|
|
||||||
use crate::task::DOWNLOADER_TASK_RUNNING;
|
|
||||||
use crate::utils::status::{PageStatus, VideoStatus};
|
|
||||||
|
|
||||||
pub fn api_router() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/api/video-sources", get(get_video_sources))
|
|
||||||
.route("/api/video-sources/details", get(get_video_sources_details))
|
|
||||||
.route("/api/video-sources/{type}/{id}", put(update_video_source))
|
|
||||||
.route("/api/video-sources/collections", post(insert_collection))
|
|
||||||
.route("/api/video-sources/favorites", post(insert_favorite))
|
|
||||||
.route("/api/video-sources/submissions", post(insert_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("/api/config", get(get_config))
|
|
||||||
.route("/api/config", put(update_config))
|
|
||||||
.route("/image-proxy", get(image_proxy))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 列出所有视频来源
|
|
||||||
pub async fn get_video_sources(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
) -> Result<ApiResponse<VideoSourcesResponse>, ApiError> {
|
|
||||||
let (collection, favorite, submission, mut watch_later) = tokio::try_join!(
|
|
||||||
collection::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.columns([collection::Column::Id, collection::Column::Name])
|
|
||||||
.into_model::<VideoSource>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
favorite::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.columns([favorite::Column::Id, favorite::Column::Name])
|
|
||||||
.into_model::<VideoSource>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
submission::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.column(submission::Column::Id)
|
|
||||||
.column_as(submission::Column::UpperName, "name")
|
|
||||||
.into_model::<VideoSource>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
watch_later::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.column(watch_later::Column::Id)
|
|
||||||
.column_as(Expr::value("稍后再看"), "name")
|
|
||||||
.into_model::<VideoSource>()
|
|
||||||
.all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
// watch_later 是一个特殊的视频来源,如果不存在则添加一个默认项
|
|
||||||
if watch_later.is_empty() {
|
|
||||||
watch_later.push(VideoSource {
|
|
||||||
id: 1,
|
|
||||||
name: "稍后再看".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(ApiResponse::ok(VideoSourcesResponse {
|
|
||||||
collection,
|
|
||||||
favorite,
|
|
||||||
submission,
|
|
||||||
watch_later,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
|
||||||
pub async fn get_videos(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
Query(params): Query<VideosRequest>,
|
|
||||||
) -> Result<ApiResponse<VideosResponse>, ApiError> {
|
|
||||||
let mut query = video::Entity::find();
|
|
||||||
for (field, column) in [
|
|
||||||
(params.collection, video::Column::CollectionId),
|
|
||||||
(params.favorite, video::Column::FavoriteId),
|
|
||||||
(params.submission, video::Column::SubmissionId),
|
|
||||||
(params.watch_later, video::Column::WatchLaterId),
|
|
||||||
] {
|
|
||||||
if let Some(id) = field {
|
|
||||||
query = query.filter(column.eq(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(query_word) = params.query {
|
|
||||||
query = query.filter(video::Column::Name.contains(query_word));
|
|
||||||
}
|
|
||||||
let total_count = query.clone().count(db.as_ref()).await?;
|
|
||||||
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
|
||||||
(page, page_size)
|
|
||||||
} else {
|
|
||||||
(0, 10)
|
|
||||||
};
|
|
||||||
Ok(ApiResponse::ok(VideosResponse {
|
|
||||||
videos: query
|
|
||||||
.order_by_desc(video::Column::Id)
|
|
||||||
.into_partial_model::<VideoInfo>()
|
|
||||||
.paginate(db.as_ref(), page_size)
|
|
||||||
.fetch_page(page)
|
|
||||||
.await?,
|
|
||||||
total_count,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_video(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
) -> Result<ApiResponse<VideoResponse>, ApiError> {
|
|
||||||
let (video_info, pages_info) = tokio::try_join!(
|
|
||||||
video::Entity::find_by_id(id)
|
|
||||||
.into_partial_model::<VideoInfo>()
|
|
||||||
.one(db.as_ref()),
|
|
||||||
page::Entity::find()
|
|
||||||
.filter(page::Column::VideoId.eq(id))
|
|
||||||
.order_by_asc(page::Column::Cid)
|
|
||||||
.into_partial_model::<PageInfo>()
|
|
||||||
.all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
let Some(video_info) = video_info else {
|
|
||||||
return Err(InnerApiError::NotFound(id).into());
|
|
||||||
};
|
|
||||||
Ok(ApiResponse::ok(VideoResponse {
|
|
||||||
video: video_info,
|
|
||||||
pages: pages_info,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reset_video(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
|
|
||||||
let (video_info, pages_info) = tokio::try_join!(
|
|
||||||
video::Entity::find_by_id(id)
|
|
||||||
.into_partial_model::<VideoInfo>()
|
|
||||||
.one(db.as_ref()),
|
|
||||||
page::Entity::find()
|
|
||||||
.filter(page::Column::VideoId.eq(id))
|
|
||||||
.order_by_asc(page::Column::Cid)
|
|
||||||
.into_partial_model::<PageInfo>()
|
|
||||||
.all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
let Some(mut video_info) = video_info else {
|
|
||||||
return Err(InnerApiError::NotFound(id).into());
|
|
||||||
};
|
|
||||||
let resetted_pages_info = pages_info
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|mut page_info| {
|
|
||||||
let mut page_status = PageStatus::from(page_info.download_status);
|
|
||||||
if page_status.reset_failed() {
|
|
||||||
page_info.download_status = page_status.into();
|
|
||||||
Some(page_info)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
||||||
let mut video_resetted = video_status.reset_failed();
|
|
||||||
if !resetted_pages_info.is_empty() {
|
|
||||||
video_status.set(4, 0); // 将“分P下载”重置为 0
|
|
||||||
video_resetted = true;
|
|
||||||
}
|
|
||||||
let resetted_videos_info = if video_resetted {
|
|
||||||
video_info.download_status = video_status.into();
|
|
||||||
vec![&video_info]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
let resetted = !resetted_videos_info.is_empty() || !resetted_pages_info.is_empty();
|
|
||||||
if resetted {
|
|
||||||
let txn = db.begin().await?;
|
|
||||||
if !resetted_videos_info.is_empty() {
|
|
||||||
// 只可能有 1 个元素,所以不用 batch
|
|
||||||
update_video_download_status(&txn, &resetted_videos_info, None).await?;
|
|
||||||
}
|
|
||||||
if !resetted_pages_info.is_empty() {
|
|
||||||
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
|
||||||
}
|
|
||||||
txn.commit().await?;
|
|
||||||
}
|
|
||||||
Ok(ApiResponse::ok(ResetVideoResponse {
|
|
||||||
resetted,
|
|
||||||
video: video_info,
|
|
||||||
pages: resetted_pages_info,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reset_all_videos(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
) -> Result<ApiResponse<ResetAllVideosResponse>, ApiError> {
|
|
||||||
// 先查询所有视频和页面数据
|
|
||||||
let (all_videos, all_pages) = tokio::try_join!(
|
|
||||||
video::Entity::find().into_partial_model::<VideoInfo>().all(db.as_ref()),
|
|
||||||
page::Entity::find().into_partial_model::<PageInfo>().all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
let resetted_pages_info = all_pages
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|mut page_info| {
|
|
||||||
let mut page_status = PageStatus::from(page_info.download_status);
|
|
||||||
if page_status.reset_failed() {
|
|
||||||
page_info.download_status = page_status.into();
|
|
||||||
Some(page_info)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let video_ids_with_resetted_pages: HashSet<i32> = resetted_pages_info.iter().map(|page| page.video_id).collect();
|
|
||||||
let resetted_videos_info = all_videos
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|mut video_info| {
|
|
||||||
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
||||||
let mut video_resetted = video_status.reset_failed();
|
|
||||||
if video_ids_with_resetted_pages.contains(&video_info.id) {
|
|
||||||
video_status.set(4, 0); // 将"分P下载"重置为 0
|
|
||||||
video_resetted = true;
|
|
||||||
}
|
|
||||||
if video_resetted {
|
|
||||||
video_info.download_status = video_status.into();
|
|
||||||
Some(video_info)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let has_video_updates = !resetted_videos_info.is_empty();
|
|
||||||
let has_page_updates = !resetted_pages_info.is_empty();
|
|
||||||
if has_video_updates || has_page_updates {
|
|
||||||
let txn = db.begin().await?;
|
|
||||||
if has_video_updates {
|
|
||||||
update_video_download_status(&txn, &resetted_videos_info, Some(500)).await?;
|
|
||||||
}
|
|
||||||
if has_page_updates {
|
|
||||||
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
|
||||||
}
|
|
||||||
txn.commit().await?;
|
|
||||||
}
|
|
||||||
Ok(ApiResponse::ok(ResetAllVideosResponse {
|
|
||||||
resetted: has_video_updates || has_page_updates,
|
|
||||||
resetted_videos_count: resetted_videos_info.len(),
|
|
||||||
resetted_pages_count: resetted_pages_info.len(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_video_status(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
ValidatedJson(request): ValidatedJson<UpdateVideoStatusRequest>,
|
|
||||||
) -> Result<ApiResponse<UpdateVideoStatusResponse>, ApiError> {
|
|
||||||
let (video_info, mut pages_info) = tokio::try_join!(
|
|
||||||
video::Entity::find_by_id(id)
|
|
||||||
.into_partial_model::<VideoInfo>()
|
|
||||||
.one(db.as_ref()),
|
|
||||||
page::Entity::find()
|
|
||||||
.filter(page::Column::VideoId.eq(id))
|
|
||||||
.order_by_asc(page::Column::Cid)
|
|
||||||
.into_partial_model::<PageInfo>()
|
|
||||||
.all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
let Some(mut video_info) = video_info else {
|
|
||||||
return Err(InnerApiError::NotFound(id).into());
|
|
||||||
};
|
|
||||||
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
||||||
for update in &request.video_updates {
|
|
||||||
video_status.set(update.status_index, update.status_value);
|
|
||||||
}
|
|
||||||
video_info.download_status = video_status.into();
|
|
||||||
let mut updated_pages_info = Vec::new();
|
|
||||||
let mut page_id_map = pages_info
|
|
||||||
.iter_mut()
|
|
||||||
.map(|page| (page.id, page))
|
|
||||||
.collect::<std::collections::HashMap<_, _>>();
|
|
||||||
for page_update in &request.page_updates {
|
|
||||||
if let Some(page_info) = page_id_map.remove(&page_update.page_id) {
|
|
||||||
let mut page_status = PageStatus::from(page_info.download_status);
|
|
||||||
for update in &page_update.updates {
|
|
||||||
page_status.set(update.status_index, update.status_value);
|
|
||||||
}
|
|
||||||
page_info.download_status = page_status.into();
|
|
||||||
updated_pages_info.push(page_info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let has_video_updates = !request.video_updates.is_empty();
|
|
||||||
let has_page_updates = !updated_pages_info.is_empty();
|
|
||||||
if has_video_updates || has_page_updates {
|
|
||||||
let txn = db.begin().await?;
|
|
||||||
if has_video_updates {
|
|
||||||
update_video_download_status(&txn, &[&video_info], None).await?;
|
|
||||||
}
|
|
||||||
if has_page_updates {
|
|
||||||
update_page_download_status(&txn, &updated_pages_info, None).await?;
|
|
||||||
}
|
|
||||||
txn.commit().await?;
|
|
||||||
}
|
|
||||||
Ok(ApiResponse::ok(UpdateVideoStatusResponse {
|
|
||||||
success: has_video_updates || has_page_updates,
|
|
||||||
video: video_info,
|
|
||||||
pages: pages_info,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
// api 返回的 id 才是真实的 fid
|
|
||||||
fid: fav.id,
|
|
||||||
mid: fav.mid,
|
|
||||||
subscribed: subscribed_set.contains(&fav.id),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
.into_iter()
|
|
||||||
.map(|col| CollectionWithSubscriptionStatus {
|
|
||||||
title: col.title,
|
|
||||||
sid: col.id,
|
|
||||||
mid: col.mid,
|
|
||||||
invalid: col.state == 1,
|
|
||||||
subscribed: subscribed_set.contains(&col.id),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ApiResponse::ok(CollectionsResponse {
|
|
||||||
collections,
|
|
||||||
total: bili_collections.count,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
// 官方没有提供字段,但是可以使用这种方式简单判断下
|
|
||||||
invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg",
|
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_favorite(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
|
||||||
ValidatedJson(request): ValidatedJson<InsertFavoriteRequest>,
|
|
||||||
) -> 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()
|
|
||||||
})
|
|
||||||
.exec(db.as_ref())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ApiResponse::ok(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_collection(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
|
||||||
ValidatedJson(request): ValidatedJson<InsertCollectionRequest>,
|
|
||||||
) -> Result<ApiResponse<bool>, ApiError> {
|
|
||||||
let collection = Collection::new(
|
|
||||||
bili_client.as_ref(),
|
|
||||||
CollectionItem {
|
|
||||||
sid: request.sid.to_string(),
|
|
||||||
mid: request.mid.to_string(),
|
|
||||||
collection_type: request.collection_type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
.exec(db.as_ref())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ApiResponse::ok(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_submission(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
|
||||||
ValidatedJson(request): ValidatedJson<InsertSubmissionRequest>,
|
|
||||||
) -> 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()
|
|
||||||
})
|
|
||||||
.exec(db.as_ref())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ApiResponse::ok(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_video_sources_details(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
) -> Result<ApiResponse<VideoSourcesDetailsResponse>, ApiError> {
|
|
||||||
let (collections, favorites, submissions, mut watch_later) = tokio::try_join!(
|
|
||||||
collection::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.columns([
|
|
||||||
collection::Column::Id,
|
|
||||||
collection::Column::Name,
|
|
||||||
collection::Column::Path,
|
|
||||||
collection::Column::Enabled
|
|
||||||
])
|
|
||||||
.into_model::<VideoSourceDetail>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
favorite::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.columns([
|
|
||||||
favorite::Column::Id,
|
|
||||||
favorite::Column::Name,
|
|
||||||
favorite::Column::Path,
|
|
||||||
favorite::Column::Enabled
|
|
||||||
])
|
|
||||||
.into_model::<VideoSourceDetail>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
submission::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.column(submission::Column::Id)
|
|
||||||
.column_as(submission::Column::UpperName, "name")
|
|
||||||
.columns([submission::Column::Path, submission::Column::Enabled])
|
|
||||||
.into_model::<VideoSourceDetail>()
|
|
||||||
.all(db.as_ref()),
|
|
||||||
watch_later::Entity::find()
|
|
||||||
.select_only()
|
|
||||||
.column(watch_later::Column::Id)
|
|
||||||
.column_as(Expr::value("稍后再看"), "name")
|
|
||||||
.columns([watch_later::Column::Path, watch_later::Column::Enabled])
|
|
||||||
.into_model::<VideoSourceDetail>()
|
|
||||||
.all(db.as_ref())
|
|
||||||
)?;
|
|
||||||
if watch_later.is_empty() {
|
|
||||||
watch_later.push(VideoSourceDetail {
|
|
||||||
id: 1,
|
|
||||||
name: "稍后再看".to_string(),
|
|
||||||
path: String::new(),
|
|
||||||
enabled: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Ok(ApiResponse::ok(VideoSourcesDetailsResponse {
|
|
||||||
collections,
|
|
||||||
favorites,
|
|
||||||
submissions,
|
|
||||||
watch_later,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_video_source(
|
|
||||||
Path((source_type, id)): Path<(String, i32)>,
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
ValidatedJson(request): ValidatedJson<UpdateVideoSourceRequest>,
|
|
||||||
) -> Result<ApiResponse<bool>, ApiError> {
|
|
||||||
let active_model = match source_type.as_str() {
|
|
||||||
"collections" => collection::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
|
||||||
let mut active_model: collection::ActiveModel = model.into();
|
|
||||||
active_model.path = Set(request.path);
|
|
||||||
active_model.enabled = Set(request.enabled);
|
|
||||||
_ActiveModel::Collection(active_model)
|
|
||||||
}),
|
|
||||||
"favorites" => favorite::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
|
||||||
let mut active_model: favorite::ActiveModel = model.into();
|
|
||||||
active_model.path = Set(request.path);
|
|
||||||
active_model.enabled = Set(request.enabled);
|
|
||||||
_ActiveModel::Favorite(active_model)
|
|
||||||
}),
|
|
||||||
"submissions" => submission::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
|
||||||
let mut active_model: submission::ActiveModel = model.into();
|
|
||||||
active_model.path = Set(request.path);
|
|
||||||
active_model.enabled = Set(request.enabled);
|
|
||||||
_ActiveModel::Submission(active_model)
|
|
||||||
}),
|
|
||||||
"watch_later" => match watch_later::Entity::find_by_id(id).one(db.as_ref()).await? {
|
|
||||||
// 稍后再看需要做特殊处理,get 时如果稍后再看不存在返回的是 id 为 1 的假记录
|
|
||||||
// 因此此处可能是更新也可能是插入,做个额外的处理
|
|
||||||
Some(model) => {
|
|
||||||
// 如果有记录,使用 id 对应的记录更新
|
|
||||||
let mut active_model: watch_later::ActiveModel = model.into();
|
|
||||||
active_model.path = Set(request.path);
|
|
||||||
active_model.enabled = Set(request.enabled);
|
|
||||||
Some(_ActiveModel::WatchLater(active_model))
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if id != 1 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// 如果没有记录且 id 为 1,插入一个新的稍后再看记录
|
|
||||||
Some(_ActiveModel::WatchLater(watch_later::ActiveModel {
|
|
||||||
id: Set(1),
|
|
||||||
path: Set(request.path),
|
|
||||||
enabled: Set(request.enabled),
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()),
|
|
||||||
};
|
|
||||||
let Some(active_model) = active_model else {
|
|
||||||
return Err(InnerApiError::NotFound(id).into());
|
|
||||||
};
|
|
||||||
active_model.save(db.as_ref()).await?;
|
|
||||||
Ok(ApiResponse::ok(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_config() -> Result<ApiResponse<Arc<Config>>, ApiError> {
|
|
||||||
Ok(ApiResponse::ok(VersionedConfig::get().load_full()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_config(
|
|
||||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
||||||
ValidatedJson(config): ValidatedJson<Config>,
|
|
||||||
) -> Result<ApiResponse<Arc<Config>>, ApiError> {
|
|
||||||
let Ok(_lock) = DOWNLOADER_TASK_RUNNING.try_lock() else {
|
|
||||||
// 简单避免一下可能的不一致现象
|
|
||||||
return Err(InnerApiError::BadRequest("下载任务正在运行,无法修改配置".to_string()).into());
|
|
||||||
};
|
|
||||||
config.check()?;
|
|
||||||
let new_config = VersionedConfig::get().update(config, db.as_ref()).await?;
|
|
||||||
drop(_lock);
|
|
||||||
Ok(ApiResponse::ok(new_config))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,8 +1,8 @@
|
|||||||
pub mod auth;
|
|
||||||
pub mod handler;
|
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod helper;
|
mod helper;
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
mod routes;
|
||||||
mod wrapper;
|
mod wrapper;
|
||||||
|
|
||||||
|
pub use routes::router;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::Extension;
|
||||||
|
use axum::routing::get;
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
use crate::api::error::InnerApiError;
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||||
|
use crate::config::{Config, VersionedConfig};
|
||||||
|
use crate::task::DOWNLOADER_TASK_RUNNING;
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new().route("/config", get(get_config).put(update_config))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取全局配置
|
||||||
|
pub async fn get_config() -> Result<ApiResponse<Arc<Config>>, ApiError> {
|
||||||
|
Ok(ApiResponse::ok(VersionedConfig::get().load_full()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新全局配置
|
||||||
|
pub async fn update_config(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
ValidatedJson(config): ValidatedJson<Config>,
|
||||||
|
) -> Result<ApiResponse<Arc<Config>>, ApiError> {
|
||||||
|
let Ok(_lock) = DOWNLOADER_TASK_RUNNING.try_lock() else {
|
||||||
|
// 简单避免一下可能的不一致现象
|
||||||
|
return Err(InnerApiError::BadRequest("下载任务正在运行,无法修改配置".to_string()).into());
|
||||||
|
};
|
||||||
|
config.check()?;
|
||||||
|
let new_config = VersionedConfig::get().update(config, db.as_ref()).await?;
|
||||||
|
drop(_lock);
|
||||||
|
Ok(ApiResponse::ok(new_config))
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{Extension, Query};
|
||||||
|
use axum::routing::get;
|
||||||
|
use bili_sync_entity::*;
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect};
|
||||||
|
|
||||||
|
use crate::api::request::{FollowedCollectionsRequest, FollowedUppersRequest};
|
||||||
|
use crate::api::response::{
|
||||||
|
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse,
|
||||||
|
UpperWithSubscriptionStatus, UppersResponse,
|
||||||
|
};
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse};
|
||||||
|
use crate::bilibili::{BiliClient, Me};
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/me/favorites", get(get_created_favorites))
|
||||||
|
.route("/me/collections", get(get_followed_collections))
|
||||||
|
.route("/me/uppers", get(get_followed_uppers))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前用户创建的收藏夹
|
||||||
|
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,
|
||||||
|
// api 返回的 id 才是真实的 fid
|
||||||
|
fid: fav.id,
|
||||||
|
mid: fav.mid,
|
||||||
|
subscribed: subscribed_set.contains(&fav.id),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前用户收藏的合集
|
||||||
|
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
|
||||||
|
.into_iter()
|
||||||
|
.map(|col| CollectionWithSubscriptionStatus {
|
||||||
|
title: col.title,
|
||||||
|
sid: col.id,
|
||||||
|
mid: col.mid,
|
||||||
|
invalid: col.state == 1,
|
||||||
|
subscribed: subscribed_set.contains(&col.id),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ApiResponse::ok(CollectionsResponse {
|
||||||
|
collections,
|
||||||
|
total: bili_collections.count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前用户关注的 UP 主
|
||||||
|
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,
|
||||||
|
// 官方没有提供字段,但是可以使用这种方式简单判断下
|
||||||
|
invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg",
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::{Extension, Query, Request};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
|
use axum::middleware::Next;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::{Router, middleware};
|
||||||
|
use reqwest::{Method, StatusCode, header};
|
||||||
|
|
||||||
|
use super::request::ImageProxyParams;
|
||||||
|
use crate::api::wrapper::ApiResponse;
|
||||||
|
use crate::bilibili::BiliClient;
|
||||||
|
use crate::config::VersionedConfig;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod me;
|
||||||
|
mod video_sources;
|
||||||
|
mod videos;
|
||||||
|
|
||||||
|
pub fn router() -> Router {
|
||||||
|
Router::new().route("/image-proxy", get(image_proxy)).nest(
|
||||||
|
"/api",
|
||||||
|
config::router()
|
||||||
|
.merge(me::router())
|
||||||
|
.merge(video_sources::router())
|
||||||
|
.merge(videos::router())
|
||||||
|
.layer(middleware::from_fn(auth)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 中间件:验证请求头中的 Authorization 是否与配置中的 auth_token 匹配
|
||||||
|
pub async fn auth(headers: HeaderMap, request: Request, next: Next) -> Result<Response, StatusCode> {
|
||||||
|
if headers
|
||||||
|
.get("Authorization")
|
||||||
|
.is_some_and(|v| v.to_str().is_ok_and(|s| s == VersionedConfig::get().load().auth_token))
|
||||||
|
{
|
||||||
|
return Ok(next.run(request).await);
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::<()>::unauthorized("auth token does not match").into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{Extension, Path};
|
||||||
|
use axum::routing::{get, post, put};
|
||||||
|
use bili_sync_entity::*;
|
||||||
|
use bili_sync_migration::Expr;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::{DatabaseConnection, EntityTrait, QuerySelect};
|
||||||
|
|
||||||
|
use crate::adapter::_ActiveModel;
|
||||||
|
use crate::api::error::InnerApiError;
|
||||||
|
use crate::api::request::{
|
||||||
|
InsertCollectionRequest, InsertFavoriteRequest, InsertSubmissionRequest, UpdateVideoSourceRequest,
|
||||||
|
};
|
||||||
|
use crate::api::response::{VideoSource, VideoSourceDetail, VideoSourcesDetailsResponse, VideoSourcesResponse};
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||||
|
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Submission};
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/video-sources", get(get_video_sources))
|
||||||
|
.route("/video-sources/details", get(get_video_sources_details))
|
||||||
|
.route("/video-sources/{type}/{id}", put(update_video_source))
|
||||||
|
.route("/video-sources/favorites", post(insert_favorite))
|
||||||
|
.route("/video-sources/collections", post(insert_collection))
|
||||||
|
.route("/video-sources/submissions", post(insert_submission))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出所有视频来源
|
||||||
|
pub async fn get_video_sources(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<VideoSourcesResponse>, ApiError> {
|
||||||
|
let (collection, favorite, submission, mut watch_later) = tokio::try_join!(
|
||||||
|
collection::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([collection::Column::Id, collection::Column::Name])
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
favorite::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([favorite::Column::Id, favorite::Column::Name])
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
submission::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(submission::Column::Id)
|
||||||
|
.column_as(submission::Column::UpperName, "name")
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
watch_later::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(watch_later::Column::Id)
|
||||||
|
.column_as(Expr::value("稍后再看"), "name")
|
||||||
|
.into_model::<VideoSource>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
// watch_later 是一个特殊的视频来源,如果不存在则添加一个默认项
|
||||||
|
if watch_later.is_empty() {
|
||||||
|
watch_later.push(VideoSource {
|
||||||
|
id: 1,
|
||||||
|
name: "稍后再看".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::ok(VideoSourcesResponse {
|
||||||
|
collection,
|
||||||
|
favorite,
|
||||||
|
submission,
|
||||||
|
watch_later,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取视频来源详情
|
||||||
|
pub async fn get_video_sources_details(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<VideoSourcesDetailsResponse>, ApiError> {
|
||||||
|
let (collections, favorites, submissions, mut watch_later) = tokio::try_join!(
|
||||||
|
collection::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([
|
||||||
|
collection::Column::Id,
|
||||||
|
collection::Column::Name,
|
||||||
|
collection::Column::Path,
|
||||||
|
collection::Column::Enabled
|
||||||
|
])
|
||||||
|
.into_model::<VideoSourceDetail>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
favorite::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.columns([
|
||||||
|
favorite::Column::Id,
|
||||||
|
favorite::Column::Name,
|
||||||
|
favorite::Column::Path,
|
||||||
|
favorite::Column::Enabled
|
||||||
|
])
|
||||||
|
.into_model::<VideoSourceDetail>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
submission::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(submission::Column::Id)
|
||||||
|
.column_as(submission::Column::UpperName, "name")
|
||||||
|
.columns([submission::Column::Path, submission::Column::Enabled])
|
||||||
|
.into_model::<VideoSourceDetail>()
|
||||||
|
.all(db.as_ref()),
|
||||||
|
watch_later::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column(watch_later::Column::Id)
|
||||||
|
.column_as(Expr::value("稍后再看"), "name")
|
||||||
|
.columns([watch_later::Column::Path, watch_later::Column::Enabled])
|
||||||
|
.into_model::<VideoSourceDetail>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
if watch_later.is_empty() {
|
||||||
|
watch_later.push(VideoSourceDetail {
|
||||||
|
id: 1,
|
||||||
|
name: "稍后再看".to_string(),
|
||||||
|
path: String::new(),
|
||||||
|
enabled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::ok(VideoSourcesDetailsResponse {
|
||||||
|
collections,
|
||||||
|
favorites,
|
||||||
|
submissions,
|
||||||
|
watch_later,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新视频来源
|
||||||
|
pub async fn update_video_source(
|
||||||
|
Path((source_type, id)): Path<(String, i32)>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
ValidatedJson(request): ValidatedJson<UpdateVideoSourceRequest>,
|
||||||
|
) -> Result<ApiResponse<bool>, ApiError> {
|
||||||
|
let active_model = match source_type.as_str() {
|
||||||
|
"collections" => collection::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
||||||
|
let mut active_model: collection::ActiveModel = model.into();
|
||||||
|
active_model.path = Set(request.path);
|
||||||
|
active_model.enabled = Set(request.enabled);
|
||||||
|
_ActiveModel::Collection(active_model)
|
||||||
|
}),
|
||||||
|
"favorites" => favorite::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
||||||
|
let mut active_model: favorite::ActiveModel = model.into();
|
||||||
|
active_model.path = Set(request.path);
|
||||||
|
active_model.enabled = Set(request.enabled);
|
||||||
|
_ActiveModel::Favorite(active_model)
|
||||||
|
}),
|
||||||
|
"submissions" => submission::Entity::find_by_id(id).one(db.as_ref()).await?.map(|model| {
|
||||||
|
let mut active_model: submission::ActiveModel = model.into();
|
||||||
|
active_model.path = Set(request.path);
|
||||||
|
active_model.enabled = Set(request.enabled);
|
||||||
|
_ActiveModel::Submission(active_model)
|
||||||
|
}),
|
||||||
|
"watch_later" => match watch_later::Entity::find_by_id(id).one(db.as_ref()).await? {
|
||||||
|
// 稍后再看需要做特殊处理,get 时如果稍后再看不存在返回的是 id 为 1 的假记录
|
||||||
|
// 因此此处可能是更新也可能是插入,做个额外的处理
|
||||||
|
Some(model) => {
|
||||||
|
// 如果有记录,使用 id 对应的记录更新
|
||||||
|
let mut active_model: watch_later::ActiveModel = model.into();
|
||||||
|
active_model.path = Set(request.path);
|
||||||
|
active_model.enabled = Set(request.enabled);
|
||||||
|
Some(_ActiveModel::WatchLater(active_model))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if id != 1 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// 如果没有记录且 id 为 1,插入一个新的稍后再看记录
|
||||||
|
Some(_ActiveModel::WatchLater(watch_later::ActiveModel {
|
||||||
|
id: Set(1),
|
||||||
|
path: Set(request.path),
|
||||||
|
enabled: Set(request.enabled),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()),
|
||||||
|
};
|
||||||
|
let Some(active_model) = active_model else {
|
||||||
|
return Err(InnerApiError::NotFound(id).into());
|
||||||
|
};
|
||||||
|
active_model.save(db.as_ref()).await?;
|
||||||
|
Ok(ApiResponse::ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新增收藏夹订阅
|
||||||
|
pub async fn insert_favorite(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||||
|
ValidatedJson(request): ValidatedJson<InsertFavoriteRequest>,
|
||||||
|
) -> 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()
|
||||||
|
})
|
||||||
|
.exec(db.as_ref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ApiResponse::ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新增合集/列表订阅
|
||||||
|
pub async fn insert_collection(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||||
|
ValidatedJson(request): ValidatedJson<InsertCollectionRequest>,
|
||||||
|
) -> Result<ApiResponse<bool>, ApiError> {
|
||||||
|
let collection = Collection::new(
|
||||||
|
bili_client.as_ref(),
|
||||||
|
CollectionItem {
|
||||||
|
sid: request.sid.to_string(),
|
||||||
|
mid: request.mid.to_string(),
|
||||||
|
collection_type: request.collection_type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
.exec(db.as_ref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ApiResponse::ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 新增投稿订阅
|
||||||
|
pub async fn insert_submission(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
Extension(bili_client): Extension<Arc<BiliClient>>,
|
||||||
|
ValidatedJson(request): ValidatedJson<InsertSubmissionRequest>,
|
||||||
|
) -> 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()
|
||||||
|
})
|
||||||
|
.exec(db.as_ref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ApiResponse::ok(true))
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::extract::{Extension, Path, Query};
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use bili_sync_entity::*;
|
||||||
|
use sea_orm::{
|
||||||
|
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
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::response::{
|
||||||
|
PageInfo, ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, VideoInfo, VideoResponse,
|
||||||
|
VideosResponse,
|
||||||
|
};
|
||||||
|
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||||
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
|
|
||||||
|
pub(super) fn router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/videos", get(get_videos))
|
||||||
|
.route("/videos/{id}", get(get_video))
|
||||||
|
.route("/videos/{id}/reset", post(reset_video))
|
||||||
|
.route("/videos/reset-all", post(reset_all_videos))
|
||||||
|
.route("/videos/{id}/update-status", post(update_video_status))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
||||||
|
pub async fn get_videos(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
Query(params): Query<VideosRequest>,
|
||||||
|
) -> Result<ApiResponse<VideosResponse>, ApiError> {
|
||||||
|
let mut query = video::Entity::find();
|
||||||
|
for (field, column) in [
|
||||||
|
(params.collection, video::Column::CollectionId),
|
||||||
|
(params.favorite, video::Column::FavoriteId),
|
||||||
|
(params.submission, video::Column::SubmissionId),
|
||||||
|
(params.watch_later, video::Column::WatchLaterId),
|
||||||
|
] {
|
||||||
|
if let Some(id) = field {
|
||||||
|
query = query.filter(column.eq(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(query_word) = params.query {
|
||||||
|
query = query.filter(video::Column::Name.contains(query_word));
|
||||||
|
}
|
||||||
|
let total_count = query.clone().count(db.as_ref()).await?;
|
||||||
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
||||||
|
(page, page_size)
|
||||||
|
} else {
|
||||||
|
(0, 10)
|
||||||
|
};
|
||||||
|
Ok(ApiResponse::ok(VideosResponse {
|
||||||
|
videos: query
|
||||||
|
.order_by_desc(video::Column::Id)
|
||||||
|
.into_partial_model::<VideoInfo>()
|
||||||
|
.paginate(db.as_ref(), page_size)
|
||||||
|
.fetch_page(page)
|
||||||
|
.await?,
|
||||||
|
total_count,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_video(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<VideoResponse>, ApiError> {
|
||||||
|
let (video_info, pages_info) = tokio::try_join!(
|
||||||
|
video::Entity::find_by_id(id)
|
||||||
|
.into_partial_model::<VideoInfo>()
|
||||||
|
.one(db.as_ref()),
|
||||||
|
page::Entity::find()
|
||||||
|
.filter(page::Column::VideoId.eq(id))
|
||||||
|
.order_by_asc(page::Column::Cid)
|
||||||
|
.into_partial_model::<PageInfo>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
let Some(video_info) = video_info else {
|
||||||
|
return Err(InnerApiError::NotFound(id).into());
|
||||||
|
};
|
||||||
|
Ok(ApiResponse::ok(VideoResponse {
|
||||||
|
video: video_info,
|
||||||
|
pages: pages_info,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset_video(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
|
||||||
|
let (video_info, pages_info) = tokio::try_join!(
|
||||||
|
video::Entity::find_by_id(id)
|
||||||
|
.into_partial_model::<VideoInfo>()
|
||||||
|
.one(db.as_ref()),
|
||||||
|
page::Entity::find()
|
||||||
|
.filter(page::Column::VideoId.eq(id))
|
||||||
|
.order_by_asc(page::Column::Cid)
|
||||||
|
.into_partial_model::<PageInfo>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
let Some(mut video_info) = video_info else {
|
||||||
|
return Err(InnerApiError::NotFound(id).into());
|
||||||
|
};
|
||||||
|
let resetted_pages_info = pages_info
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mut page_info| {
|
||||||
|
let mut page_status = PageStatus::from(page_info.download_status);
|
||||||
|
if page_status.reset_failed() {
|
||||||
|
page_info.download_status = page_status.into();
|
||||||
|
Some(page_info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
||||||
|
let mut video_resetted = video_status.reset_failed();
|
||||||
|
if !resetted_pages_info.is_empty() {
|
||||||
|
video_status.set(4, 0); // 将“分P下载”重置为 0
|
||||||
|
video_resetted = true;
|
||||||
|
}
|
||||||
|
let resetted_videos_info = if video_resetted {
|
||||||
|
video_info.download_status = video_status.into();
|
||||||
|
vec![&video_info]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
let resetted = !resetted_videos_info.is_empty() || !resetted_pages_info.is_empty();
|
||||||
|
if resetted {
|
||||||
|
let txn = db.begin().await?;
|
||||||
|
if !resetted_videos_info.is_empty() {
|
||||||
|
// 只可能有 1 个元素,所以不用 batch
|
||||||
|
update_video_download_status(&txn, &resetted_videos_info, None).await?;
|
||||||
|
}
|
||||||
|
if !resetted_pages_info.is_empty() {
|
||||||
|
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
||||||
|
}
|
||||||
|
txn.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::ok(ResetVideoResponse {
|
||||||
|
resetted,
|
||||||
|
video: video_info,
|
||||||
|
pages: resetted_pages_info,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset_all_videos(
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
) -> Result<ApiResponse<ResetAllVideosResponse>, ApiError> {
|
||||||
|
// 先查询所有视频和页面数据
|
||||||
|
let (all_videos, all_pages) = tokio::try_join!(
|
||||||
|
video::Entity::find().into_partial_model::<VideoInfo>().all(db.as_ref()),
|
||||||
|
page::Entity::find().into_partial_model::<PageInfo>().all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
let resetted_pages_info = all_pages
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mut page_info| {
|
||||||
|
let mut page_status = PageStatus::from(page_info.download_status);
|
||||||
|
if page_status.reset_failed() {
|
||||||
|
page_info.download_status = page_status.into();
|
||||||
|
Some(page_info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let video_ids_with_resetted_pages: HashSet<i32> = resetted_pages_info.iter().map(|page| page.video_id).collect();
|
||||||
|
let resetted_videos_info = all_videos
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|mut video_info| {
|
||||||
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
||||||
|
let mut video_resetted = video_status.reset_failed();
|
||||||
|
if video_ids_with_resetted_pages.contains(&video_info.id) {
|
||||||
|
video_status.set(4, 0); // 将"分P下载"重置为 0
|
||||||
|
video_resetted = true;
|
||||||
|
}
|
||||||
|
if video_resetted {
|
||||||
|
video_info.download_status = video_status.into();
|
||||||
|
Some(video_info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let has_video_updates = !resetted_videos_info.is_empty();
|
||||||
|
let has_page_updates = !resetted_pages_info.is_empty();
|
||||||
|
if has_video_updates || has_page_updates {
|
||||||
|
let txn = db.begin().await?;
|
||||||
|
if has_video_updates {
|
||||||
|
update_video_download_status(&txn, &resetted_videos_info, Some(500)).await?;
|
||||||
|
}
|
||||||
|
if has_page_updates {
|
||||||
|
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
||||||
|
}
|
||||||
|
txn.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::ok(ResetAllVideosResponse {
|
||||||
|
resetted: has_video_updates || has_page_updates,
|
||||||
|
resetted_videos_count: resetted_videos_info.len(),
|
||||||
|
resetted_pages_count: resetted_pages_info.len(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_video_status(
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||||
|
ValidatedJson(request): ValidatedJson<UpdateVideoStatusRequest>,
|
||||||
|
) -> Result<ApiResponse<UpdateVideoStatusResponse>, ApiError> {
|
||||||
|
let (video_info, mut pages_info) = tokio::try_join!(
|
||||||
|
video::Entity::find_by_id(id)
|
||||||
|
.into_partial_model::<VideoInfo>()
|
||||||
|
.one(db.as_ref()),
|
||||||
|
page::Entity::find()
|
||||||
|
.filter(page::Column::VideoId.eq(id))
|
||||||
|
.order_by_asc(page::Column::Cid)
|
||||||
|
.into_partial_model::<PageInfo>()
|
||||||
|
.all(db.as_ref())
|
||||||
|
)?;
|
||||||
|
let Some(mut video_info) = video_info else {
|
||||||
|
return Err(InnerApiError::NotFound(id).into());
|
||||||
|
};
|
||||||
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
||||||
|
for update in &request.video_updates {
|
||||||
|
video_status.set(update.status_index, update.status_value);
|
||||||
|
}
|
||||||
|
video_info.download_status = video_status.into();
|
||||||
|
let mut updated_pages_info = Vec::new();
|
||||||
|
let mut page_id_map = pages_info
|
||||||
|
.iter_mut()
|
||||||
|
.map(|page| (page.id, page))
|
||||||
|
.collect::<std::collections::HashMap<_, _>>();
|
||||||
|
for page_update in &request.page_updates {
|
||||||
|
if let Some(page_info) = page_id_map.remove(&page_update.page_id) {
|
||||||
|
let mut page_status = PageStatus::from(page_info.download_status);
|
||||||
|
for update in &page_update.updates {
|
||||||
|
page_status.set(update.status_index, update.status_value);
|
||||||
|
}
|
||||||
|
page_info.download_status = page_status.into();
|
||||||
|
updated_pages_info.push(page_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let has_video_updates = !request.video_updates.is_empty();
|
||||||
|
let has_page_updates = !updated_pages_info.is_empty();
|
||||||
|
if has_video_updates || has_page_updates {
|
||||||
|
let txn = db.begin().await?;
|
||||||
|
if has_video_updates {
|
||||||
|
update_video_download_status(&txn, &[&video_info], None).await?;
|
||||||
|
}
|
||||||
|
if has_page_updates {
|
||||||
|
update_page_download_status(&txn, &updated_pages_info, None).await?;
|
||||||
|
}
|
||||||
|
txn.commit().await?;
|
||||||
|
}
|
||||||
|
Ok(ApiResponse::ok(UpdateVideoStatusResponse {
|
||||||
|
success: has_video_updates || has_page_updates,
|
||||||
|
video: video_info,
|
||||||
|
pages: pages_info,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -5,13 +5,12 @@ use axum::extract::Request;
|
|||||||
use axum::http::{Uri, header};
|
use axum::http::{Uri, header};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{Extension, Router, ServiceExt, middleware};
|
use axum::{Extension, ServiceExt};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use rust_embed_for_web::{EmbedableFile, RustEmbed};
|
use rust_embed_for_web::{EmbedableFile, RustEmbed};
|
||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
use crate::api::auth;
|
use crate::api::router;
|
||||||
use crate::api::handler::api_router;
|
|
||||||
use crate::bilibili::BiliClient;
|
use crate::bilibili::BiliClient;
|
||||||
use crate::config::VersionedConfig;
|
use crate::config::VersionedConfig;
|
||||||
|
|
||||||
@@ -22,12 +21,10 @@ use crate::config::VersionedConfig;
|
|||||||
struct Asset;
|
struct Asset;
|
||||||
|
|
||||||
pub async fn http_server(database_connection: Arc<DatabaseConnection>, bili_client: Arc<BiliClient>) -> Result<()> {
|
pub async fn http_server(database_connection: Arc<DatabaseConnection>, bili_client: Arc<BiliClient>) -> Result<()> {
|
||||||
let app = Router::new()
|
let app = router()
|
||||||
.merge(api_router())
|
|
||||||
.fallback_service(get(frontend_files))
|
.fallback_service(get(frontend_files))
|
||||||
.layer(Extension(database_connection))
|
.layer(Extension(database_connection))
|
||||||
.layer(Extension(bili_client))
|
.layer(Extension(bili_client));
|
||||||
.layer(middleware::from_fn(auth::auth));
|
|
||||||
let config = VersionedConfig::get().load_full();
|
let config = VersionedConfig::get().load_full();
|
||||||
let listener = tokio::net::TcpListener::bind(&config.bind_address)
|
let listener = tokio::net::TcpListener::bind(&config.bind_address)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user