feat: 支持手动编辑某个视频、分页状态,优化部分代码 (#355)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-06-06 07:39:17 +08:00
committed by GitHub
parent c0ed37750f
commit 65a047b0fa
20 changed files with 670 additions and 31 deletions

View File

@@ -4,4 +4,6 @@ use thiserror::Error;
pub enum InnerApiError {
#[error("Primary key not found: {0}")]
NotFound(i32),
#[error("Bad request: {0}")]
BadRequest(String),
}

View File

@@ -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,
}))
}

View File

@@ -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(());
}

View File

@@ -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>,
}

View File

@@ -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,

View File

@@ -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))
}
}

View File

@@ -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/")

View File

@@ -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) {

View File

@@ -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;
}
}

View 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",
))
}
}