feat: 支持手动编辑某个视频、分页状态,优化部分代码 (#355)
This commit is contained in:
@@ -4,4 +4,6 @@ use thiserror::Error;
|
||||
pub enum InnerApiError {
|
||||
#[error("Primary key not found: {0}")]
|
||||
NotFound(i32),
|
||||
#[error("Bad request: {0}")]
|
||||
BadRequest(String),
|
||||
}
|
||||
|
||||
@@ -14,17 +14,17 @@ use utoipa::OpenApi;
|
||||
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::VideosRequest;
|
||||
use crate::api::request::{ResetVideoStatusRequest, VideosRequest};
|
||||
use crate::api::response::{
|
||||
PageInfo, ResetAllVideosResponse, ResetVideoResponse, VideoInfo, VideoResponse, VideoSource, VideoSourcesResponse,
|
||||
VideosResponse,
|
||||
PageInfo, ResetAllVideosResponse, ResetVideoResponse, ResetVideoStatusResponse, VideoInfo, VideoResponse,
|
||||
VideoSource, VideoSourcesResponse, VideosResponse,
|
||||
};
|
||||
use crate::api::wrapper::{ApiError, ApiResponse};
|
||||
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||
use crate::utils::status::{PageStatus, VideoStatus};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(get_video_sources, get_videos, get_video, reset_video, reset_all_videos),
|
||||
paths(get_video_sources, get_videos, get_video, reset_video, reset_all_videos, reset_video_status),
|
||||
modifiers(&OpenAPIAuth),
|
||||
security(
|
||||
("Token" = []),
|
||||
@@ -197,7 +197,7 @@ pub async fn reset_video(
|
||||
}
|
||||
let resetted_videos_info = if video_resetted {
|
||||
video_info.download_status = video_status.into();
|
||||
vec![video_info.clone()]
|
||||
vec![&video_info]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -283,3 +283,69 @@ pub async fn reset_all_videos(
|
||||
resetted_pages_count: resetted_pages_info.len(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 重置指定视频及其分页的指定状态位
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/videos/{id}/reset-status",
|
||||
request_body = ResetVideoStatusRequest,
|
||||
responses(
|
||||
(status = 200, body = ApiResponse<ResetVideoStatusResponse>),
|
||||
)
|
||||
)]
|
||||
pub async fn reset_video_status(
|
||||
Path(id): Path<i32>,
|
||||
Extension(db): Extension<Arc<DatabaseConnection>>,
|
||||
ValidatedJson(request): ValidatedJson<ResetVideoStatusRequest>,
|
||||
) -> Result<ApiResponse<ResetVideoStatusResponse>, 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(ResetVideoStatusResponse {
|
||||
success: has_video_updates || has_page_updates,
|
||||
video: video_info,
|
||||
pages: pages_info,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,44 +1,48 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use sea_orm::{ConnectionTrait, DatabaseTransaction};
|
||||
|
||||
use crate::api::response::{PageInfo, VideoInfo};
|
||||
|
||||
pub async fn update_video_download_status(
|
||||
txn: &DatabaseTransaction,
|
||||
videos: &[VideoInfo],
|
||||
videos: &[impl Borrow<VideoInfo>],
|
||||
batch_size: Option<usize>,
|
||||
) -> Result<(), sea_orm::DbErr> {
|
||||
if videos.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let videos = videos.iter().map(|v| v.borrow()).collect::<Vec<_>>();
|
||||
if let Some(size) = batch_size {
|
||||
for chunk in videos.chunks(size) {
|
||||
execute_video_update_batch(txn, chunk).await?;
|
||||
}
|
||||
} else {
|
||||
execute_video_update_batch(txn, videos).await?;
|
||||
execute_video_update_batch(txn, &videos).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_page_download_status(
|
||||
txn: &DatabaseTransaction,
|
||||
pages: &[PageInfo],
|
||||
pages: &[impl Borrow<PageInfo>],
|
||||
batch_size: Option<usize>,
|
||||
) -> Result<(), sea_orm::DbErr> {
|
||||
if pages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let pages = pages.iter().map(|v| v.borrow()).collect::<Vec<_>>();
|
||||
if let Some(size) = batch_size {
|
||||
for chunk in pages.chunks(size) {
|
||||
execute_page_update_batch(txn, chunk).await?;
|
||||
}
|
||||
} else {
|
||||
execute_page_update_batch(txn, pages).await?;
|
||||
execute_page_update_batch(txn, &pages).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_video_update_batch(txn: &DatabaseTransaction, videos: &[VideoInfo]) -> Result<(), sea_orm::DbErr> {
|
||||
async fn execute_video_update_batch(txn: &DatabaseTransaction, videos: &[&VideoInfo]) -> Result<(), sea_orm::DbErr> {
|
||||
if videos.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -58,7 +62,7 @@ async fn execute_video_update_batch(txn: &DatabaseTransaction, videos: &[VideoIn
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_page_update_batch(txn: &DatabaseTransaction, pages: &[PageInfo]) -> Result<(), sea_orm::DbErr> {
|
||||
async fn execute_page_update_batch(txn: &DatabaseTransaction, pages: &[&PageInfo]) -> Result<(), sea_orm::DbErr> {
|
||||
if pages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use validator::Validate;
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct VideosRequest {
|
||||
pub collection: Option<i32>,
|
||||
@@ -11,3 +11,28 @@ pub struct VideosRequest {
|
||||
pub page: Option<u64>,
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate, ToSchema)]
|
||||
pub struct StatusUpdate {
|
||||
#[validate(range(min = 0, max = 4))]
|
||||
pub status_index: usize,
|
||||
#[validate(custom(function = "crate::utils::validation::validate_status_value"))]
|
||||
pub status_value: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, Validate)]
|
||||
pub struct PageStatusUpdate {
|
||||
pub page_id: i32,
|
||||
#[validate(nested)]
|
||||
pub updates: Vec<StatusUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema, Validate)]
|
||||
pub struct ResetVideoStatusRequest {
|
||||
#[serde(default)]
|
||||
#[validate(nested)]
|
||||
pub video_updates: Vec<StatusUpdate>,
|
||||
#[serde(default)]
|
||||
#[validate(nested)]
|
||||
pub page_updates: Vec<PageStatusUpdate>,
|
||||
}
|
||||
|
||||
@@ -39,13 +39,20 @@ pub struct ResetAllVideosResponse {
|
||||
pub resetted_pages_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ResetVideoStatusResponse {
|
||||
pub success: bool,
|
||||
pub video: VideoInfo,
|
||||
pub pages: Vec<PageInfo>,
|
||||
}
|
||||
|
||||
#[derive(FromQueryResult, Serialize, ToSchema)]
|
||||
pub struct VideoSource {
|
||||
id: i32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, DerivePartialModel, FromQueryResult, Clone)]
|
||||
#[derive(Serialize, ToSchema, DerivePartialModel, FromQueryResult)]
|
||||
#[sea_orm(entity = "video::Entity")]
|
||||
pub struct VideoInfo {
|
||||
pub id: i32,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use anyhow::Error;
|
||||
use axum::Json;
|
||||
use axum::extract::rejection::JsonRejection;
|
||||
use axum::extract::{FromRequest, Request};
|
||||
use axum::response::IntoResponse;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::api::error::InnerApiError;
|
||||
|
||||
@@ -18,6 +22,10 @@ impl<T: Serialize> ApiResponse<T> {
|
||||
Self { status_code: 200, data }
|
||||
}
|
||||
|
||||
pub fn bad_request(data: T) -> Self {
|
||||
Self { status_code: 400, data }
|
||||
}
|
||||
|
||||
pub fn unauthorized(data: T) -> Self {
|
||||
Self { status_code: 401, data }
|
||||
}
|
||||
@@ -57,8 +65,31 @@ impl IntoResponse for ApiError {
|
||||
if let Some(inner_error) = self.0.downcast_ref::<InnerApiError>() {
|
||||
match inner_error {
|
||||
InnerApiError::NotFound(_) => return ApiResponse::not_found(self.0.to_string()).into_response(),
|
||||
InnerApiError::BadRequest(_) => {
|
||||
return ApiResponse::bad_request(self.0.to_string()).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
ApiResponse::internal_server_error(self.0.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ValidatedJson<T>(pub T);
|
||||
|
||||
impl<T, S> FromRequest<S> for ValidatedJson<T>
|
||||
where
|
||||
T: DeserializeOwned + Validate,
|
||||
S: Send + Sync,
|
||||
Json<T>: FromRequest<S, Rejection = JsonRejection>,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let Json(value) = Json::<T>::from_request(req, state).await?;
|
||||
value
|
||||
.validate()
|
||||
.map_err(|e| ApiError::from(InnerApiError::BadRequest(e.to_string())))?;
|
||||
Ok(ValidatedJson(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ 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};
|
||||
use crate::api::handler::{
|
||||
ApiDoc, get_video, get_video_sources, get_videos, reset_all_videos, reset_video, reset_video_status,
|
||||
};
|
||||
use crate::config::CONFIG;
|
||||
|
||||
#[derive(Embed)]
|
||||
@@ -26,6 +28,7 @@ pub async fn http_server(database_connection: Arc<DatabaseConnection>) -> Result
|
||||
.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}/reset-status", post(reset_video_status))
|
||||
.route("/api/videos/reset-all", post(reset_all_videos))
|
||||
.merge(
|
||||
SwaggerUi::new("/swagger-ui/")
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod model;
|
||||
pub mod nfo;
|
||||
pub mod signal;
|
||||
pub mod status;
|
||||
|
||||
pub mod validation;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
pub fn init_logger(log_level: &str) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::error::ExecutionStatus;
|
||||
|
||||
pub static STATUS_NOT_STARTED: u32 = 0b000;
|
||||
pub(super) static STATUS_MAX_RETRY: u32 = 0b100;
|
||||
pub static STATUS_OK: u32 = 0b111;
|
||||
pub static STATUS_COMPLETED: u32 = 1 << 31;
|
||||
@@ -34,7 +35,7 @@ impl<const N: usize> Status<N> {
|
||||
for i in 0..N {
|
||||
let status = self.get_status(i);
|
||||
if !(status < STATUS_MAX_RETRY || status == STATUS_OK) {
|
||||
self.set_status(i, 0);
|
||||
self.set_status(i, STATUS_NOT_STARTED);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
13
crates/bili_sync/src/utils/validation.rs
Normal file
13
crates/bili_sync/src/utils/validation.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use validator::ValidationError;
|
||||
|
||||
use crate::utils::status::{STATUS_NOT_STARTED, STATUS_OK};
|
||||
|
||||
pub fn validate_status_value(value: u32) -> Result<(), ValidationError> {
|
||||
if value == STATUS_OK || value == STATUS_NOT_STARTED {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ValidationError::new(
|
||||
"status_value must be either STATUS_OK or STATUS_NOT_STARTED",
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user