feat: 添加视频源管理页,支持修改路径与启用状态 (#369)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-06-17 18:55:45 +08:00
committed by GitHub
parent f47ce92a51
commit 28971c3ff3
21 changed files with 769 additions and 16 deletions

View File

@@ -6,7 +6,7 @@ use axum::Router;
use axum::body::Body;
use axum::extract::{Extension, Path, Query};
use axum::response::Response;
use axum::routing::{get, post};
use axum::routing::{get, post, put};
use bili_sync_entity::*;
use bili_sync_migration::{Expr, OnConflict};
use reqwest::{Method, StatusCode, header};
@@ -18,17 +18,19 @@ use sea_orm::{
use utoipa::OpenApi;
use super::request::ImageProxyParams;
use crate::adapter::_ActiveModel;
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::{
FollowedCollectionsRequest, FollowedUppersRequest, UpdateVideoStatusRequest, UpsertCollectionRequest,
UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest,
FollowedCollectionsRequest, FollowedUppersRequest, UpdateVideoSourceRequest, UpdateVideoStatusRequest,
UpsertCollectionRequest, UpsertFavoriteRequest, UpsertSubmissionRequest, VideosRequest,
};
use crate::api::response::{
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, PageInfo,
ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, UpperWithSubscriptionStatus, UppersResponse,
VideoInfo, VideoResponse, VideoSource, VideoSourcesResponse, VideosResponse,
VideoInfo, VideoResponse, VideoSource, VideoSourceDetail, VideoSourcesDetailsResponse, VideoSourcesResponse,
VideosResponse,
};
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Me, Submission};
@@ -37,7 +39,7 @@ 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,
get_video_sources, get_video_sources_details, update_video_source, 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
),
@@ -51,6 +53,8 @@ pub struct ApiDoc;
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(upsert_collection))
.route("/api/video-sources/favorites", post(upsert_favorite))
.route("/api/video-sources/submissions", post(upsert_submission))
@@ -76,7 +80,7 @@ pub fn api_router() -> Router {
pub async fn get_video_sources(
Extension(db): Extension<Arc<DatabaseConnection>>,
) -> Result<ApiResponse<VideoSourcesResponse>, ApiError> {
let (collection, favorite, submission, watch_later) = tokio::try_join!(
let (collection, favorite, submission, mut watch_later) = tokio::try_join!(
collection::Entity::find()
.select_only()
.columns([collection::Column::Id, collection::Column::Name])
@@ -100,6 +104,13 @@ pub async fn get_video_sources(
.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,
@@ -643,6 +654,135 @@ pub async fn upsert_submission(
Ok(ApiResponse::ok(true))
}
/// 获取所有视频源的详细信息,包括 path 和 enabled 状态
#[utoipa::path(
get,
path = "/api/video-sources/details",
responses(
(status = 200, body = ApiResponse<VideoSourcesDetailsResponse>),
)
)]
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,
}))
}
/// 更新视频源的 path 和 enabled 状态
#[utoipa::path(
put,
path = "/api/video-sources/{type}/{id}",
request_body = UpdateVideoSourceRequest,
responses(
(status = 200, body = ApiResponse<bool>),
)
)]
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))
}
/// B 站的图片会检查 referer需要做个转发伪造一下否则直接返回 403
pub async fn image_proxy(
Extension(bili_client): Extension<Arc<BiliClient>>,

View File

@@ -80,3 +80,10 @@ pub struct UpsertSubmissionRequest {
pub struct ImageProxyParams {
pub url: String,
}
#[derive(Deserialize, ToSchema, Validate)]
pub struct UpdateVideoSourceRequest {
#[validate(custom(function = "crate::utils::validation::validate_path"))]
pub path: String,
pub enabled: bool,
}

View File

@@ -48,8 +48,8 @@ pub struct UpdateVideoStatusResponse {
#[derive(FromQueryResult, Serialize, ToSchema)]
pub struct VideoSource {
id: i32,
name: String,
pub id: i32,
pub name: String,
}
#[derive(Serialize, ToSchema, DerivePartialModel, FromQueryResult)]
@@ -135,3 +135,19 @@ pub struct UppersResponse {
pub uppers: Vec<UpperWithSubscriptionStatus>,
pub total: i64,
}
#[derive(Serialize, ToSchema)]
pub struct VideoSourcesDetailsResponse {
pub collections: Vec<VideoSourceDetail>,
pub favorites: Vec<VideoSourceDetail>,
pub submissions: Vec<VideoSourceDetail>,
pub watch_later: Vec<VideoSourceDetail>,
}
#[derive(Serialize, ToSchema, FromQueryResult)]
pub struct VideoSourceDetail {
pub id: i32,
pub name: String,
pub path: String,
pub enabled: bool,
}