feat: 重置任务状态时支持 force 参数,默认不启用 (#388)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-07-11 19:01:01 +08:00
committed by GitHub
parent 267e9373f9
commit adc2e32e58
14 changed files with 185 additions and 57 deletions

View File

@@ -2,6 +2,7 @@ use serde::Deserialize;
use validator::Validate;
use crate::bilibili::CollectionType;
#[derive(Deserialize)]
pub struct VideosRequest {
pub collection: Option<i32>,
@@ -13,6 +14,12 @@ pub struct VideosRequest {
pub page_size: Option<u64>,
}
#[derive(Deserialize)]
pub struct ResetRequest {
#[serde(default)]
pub force: bool,
}
#[derive(Deserialize, Validate)]
pub struct StatusUpdate {
#[validate(range(min = 0, max = 4))]

View File

@@ -2,9 +2,9 @@ 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 axum::{Json, Router};
use bili_sync_entity::*;
use sea_orm::{
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, TransactionTrait,
@@ -12,7 +12,7 @@ use sea_orm::{
use crate::api::error::InnerApiError;
use crate::api::helper::{update_page_download_status, update_video_download_status};
use crate::api::request::{UpdateVideoStatusRequest, VideosRequest};
use crate::api::request::{ResetRequest, UpdateVideoStatusRequest, VideosRequest};
use crate::api::response::{
PageInfo, ResetAllVideosResponse, ResetVideoResponse, UpdateVideoStatusResponse, VideoInfo, VideoResponse,
VideosResponse,
@@ -91,6 +91,7 @@ pub async fn get_video(
pub async fn reset_video(
Path(id): Path<i32>,
Extension(db): Extension<Arc<DatabaseConnection>>,
Json(request): Json<ResetRequest>,
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
let (video_info, pages_info) = tokio::try_join!(
video::Entity::find_by_id(id)
@@ -109,7 +110,7 @@ pub async fn reset_video(
.into_iter()
.filter_map(|mut page_info| {
let mut page_status = PageStatus::from(page_info.download_status);
if page_status.reset_failed() {
if (request.force && page_status.force_reset_failed()) || page_status.reset_failed() {
page_info.download_status = page_status.into();
Some(page_info)
} else {
@@ -118,9 +119,9 @@ pub async fn reset_video(
})
.collect::<Vec<_>>();
let mut video_status = VideoStatus::from(video_info.download_status);
let mut video_resetted = video_status.reset_failed();
let mut video_resetted = (request.force && video_status.force_reset_failed()) || video_status.reset_failed();
if !resetted_pages_info.is_empty() {
video_status.set(4, 0); // 将“分P下载”重置为 0
video_status.set(4, 0); // 将“分下载”重置为 0
video_resetted = true;
}
let resetted_videos_info = if video_resetted {
@@ -150,6 +151,7 @@ pub async fn reset_video(
pub async fn reset_all_videos(
Extension(db): Extension<Arc<DatabaseConnection>>,
Json(request): Json<ResetRequest>,
) -> Result<ApiResponse<ResetAllVideosResponse>, ApiError> {
// 先查询所有视频和页面数据
let (all_videos, all_pages) = tokio::try_join!(
@@ -160,7 +162,7 @@ pub async fn reset_all_videos(
.into_iter()
.filter_map(|mut page_info| {
let mut page_status = PageStatus::from(page_info.download_status);
if page_status.reset_failed() {
if (request.force && page_status.force_reset_failed()) || page_status.reset_failed() {
page_info.download_status = page_status.into();
Some(page_info)
} else {
@@ -173,9 +175,10 @@ pub async fn reset_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();
let mut video_resetted =
(request.force && video_status.force_reset_failed()) || video_status.reset_failed();
if video_ids_with_resetted_pages.contains(&video_info.id) {
video_status.set(4, 0); // 将"分P下载"重置为 0
video_status.set(4, 0); // 将"分下载"重置为 0
video_resetted = true;
}
if video_resetted {

View File

@@ -39,7 +39,15 @@ impl<const N: usize> Status<N> {
changed = true;
}
}
// 理论上 changed 可以直接从上面的循环中得到,因为 completed 标志位的改变是由子任务状态的改变引起的,子任务没有改变则 completed 也不会改变
changed
}
/// 重置所有失败的状态,将状态设置为 0b000返回值表示 status 是否发生了变化
/// force 版本在普通版本的基础上,会额外检查是否存在需要运行的任务,如果存在则修正 completed 标记位为“未完成”
/// 这个方法的典型用例是在引入新的任务状态后重置历史视频,允许历史视频执行新引入的任务
pub fn force_reset_failed(&mut self) -> bool {
let mut changed = self.reset_failed();
// 理论上上面的 changed 就足够了,因为 completed 标志位的改变是由子任务状态的改变引起的,子任务没有改变则 completed 也不会改变
// 但考虑特殊情况,新版本引入了一个新的子任务项,此时会出现明明有子任务未执行,但 completed 标记位仍然为 true 的情况
// 当然可以在新版本迁移文件中全局重置 completed 标记位,但这样影响范围太大感觉不太好
// 在后面进行这部分额外判断可以兼容这种情况,在由用户手动触发的 reset_failed 调用中修正 completed 标记位
@@ -161,7 +169,7 @@ impl<const N: usize> From<[u32; N]> for Status<N> {
}
}
/// 包含五个子任务从前到后依次是视频封面、视频信息、Up 主头像、Up 主信息、分 P 下载
/// 包含五个子任务从前到后依次是视频封面、视频信息、Up 主头像、Up 主信息、分下载
pub type VideoStatus = Status<5>;
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
@@ -233,10 +241,13 @@ mod test {
assert!(status.reset_failed());
assert!(!status.get_completed());
assert_eq!(<[u32; 3]>::from(status), [3, 0, 7]);
// 没有内容需要重置,但 completed 标记位是错误的(模拟新增一个子任务状态的情况),此时 reset_failed 会修正 completed 标记位
// 没有内容需要重置,但 completed 标记位是错误的(模拟新增一个子任务状态的情况)
// 此时 reset_failed 不会修正 completed 标记位,而 force_reset_failed 会
status.set_completed(true);
assert!(status.get_completed());
assert!(status.reset_failed());
assert!(!status.reset_failed());
assert!(status.get_completed());
assert!(status.force_reset_failed());
assert!(!status.get_completed());
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
let mut status = Status::<3>::from([7, 7, 7]);

View File

@@ -256,7 +256,7 @@ pub async fn download_video_pages(
&video_model,
base_upper_path.join("person.nfo"),
),
// 分发并执行分 P 下载的任务
// 分发并执行分下载的任务
dispatch_download_page(
separate_status[4],
bili_client,
@@ -332,10 +332,10 @@ pub async fn dispatch_download_page(
.take_while(|res| {
match res {
Ok(model) => {
// 该视频的所有分页的下载状态都会在此返回,需要根据这些状态确认视频层“分 P 下载”子任务的状态
// 该视频的所有分页的下载状态都会在此返回,需要根据这些状态确认视频层“分下载”子任务的状态
// 在过去的实现中,此处仅仅根据 page_download_status 的最高标志位来判断,如果最高标志位是 true 则认为完成
// 这样会导致即使分页中有失败到 MAX_RETRY 的情况,视频层的分 P 下载状态也会被认为是 Succeeded不够准确
// 新版本实现会将此处取值为所有子任务状态的最小值,这样只有所有分页的子任务全部成功时才会认为视频层的分 P 下载状态是 Succeeded
// 这样会导致即使分页中有失败到 MAX_RETRY 的情况,视频层的分下载状态也会被认为是 Succeeded不够准确
// 新版本实现会将此处取值为所有子任务状态的最小值,这样只有所有分页的子任务全部成功时才会认为视频层的分下载状态是 Succeeded
let page_download_status = model.download_status.try_as_ref().expect("download_status must be set");
let separate_status: [u32; 5] = PageStatus::from(*page_download_status).into();
for status in separate_status {