feat: videos页面中新增仅失败过滤选项 (#605)

* videos页面中新增 仅失败过滤选项

* 仅失败筛选时才计算失败标记,避免额外的分页查询

* 去除[仅失败]多余的逻辑判定

* refactor: 后端调整:1)为 status -> sql 加入一个中间层方便拓展;2)将 Option<bool> 改为带有 default 的 bool;3)failed 统一改成 failed_only

* refactor: 前端调整:1)前端也统一改成 failed_only;2)修复很多地方在 loadVideo 前没有读取 failedOnly;3)略微调整前端样式

* format

---------

Co-authored-by: kaixin1995 <admin@haokaikai.cn>
Co-authored-by: amtoaer <amtoaer@gmail.com>
This commit is contained in:
开心
2026-01-13 22:28:10 +08:00
committed by GitHub
parent c05463285b
commit 4c1abcf48c
6 changed files with 155 additions and 45 deletions

View File

@@ -11,6 +11,8 @@ pub struct VideosRequest {
pub submission: Option<i32>,
pub watch_later: Option<i32>,
pub query: Option<String>,
#[serde(default)]
pub failed_only: bool,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
@@ -29,6 +31,8 @@ pub struct ResetFilteredVideoStatusRequest {
pub watch_later: Option<i32>,
pub query: Option<String>,
#[serde(default)]
pub failed_only: bool,
#[serde(default)]
pub force: bool,
}
@@ -65,6 +69,8 @@ pub struct UpdateFilteredVideoStatusRequest {
pub watch_later: Option<i32>,
pub query: Option<String>,
#[serde(default)]
pub failed_only: bool,
#[serde(default)]
#[validate(nested)]
pub video_updates: Vec<StatusUpdate>,
#[serde(default)]

View File

@@ -62,6 +62,9 @@ pub async fn get_videos(
.or(video::Column::Bvid.contains(query_word)),
);
}
if params.failed_only {
query = query.filter(VideoStatus::query_builder().any_failed())
}
let total_count = query.clone().count(&db).await?;
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
(page, page_size)
@@ -218,6 +221,9 @@ pub async fn reset_filtered_video_status(
.or(video::Column::Bvid.contains(query_word)),
);
}
if request.failed_only {
query = query.filter(VideoStatus::query_builder().any_failed());
}
let all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
let all_pages = page::Entity::find()
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
@@ -351,6 +357,9 @@ pub async fn update_filtered_video_status(
.or(video::Column::Bvid.contains(query_word)),
);
}
if request.failed_only {
query = query.filter(VideoStatus::query_builder().any_failed())
}
let mut all_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
let mut all_pages = page::Entity::find()
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))

View File

@@ -1,3 +1,10 @@
use std::marker::PhantomData;
use bili_sync_entity::{page, video};
use bili_sync_migration::ExprTrait;
use sea_orm::sea_query::Expr;
use sea_orm::{ColumnTrait, Condition};
use crate::error::ExecutionStatus;
pub static STATUS_NOT_STARTED: u32 = 0b000;
@@ -11,10 +18,17 @@ pub static STATUS_COMPLETED: u32 = 1 << 31;
/// 如果子任务执行成功,将状态设置为 0b111该值定义为 STATUS_OK。
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
/// 当所有子任务都已经完成时,为最高位打上标记 1表示整个下载任务已经完成。
#[derive(Clone, Copy, Default)]
pub struct Status<const N: usize>(u32);
#[derive(Clone, Copy)]
pub struct Status<const N: usize, C>(u32, PhantomData<C>);
impl<const N: usize> Status<N> {
impl<const N: usize, C> Default for Status<N, C> {
fn default() -> Self {
Self(0, PhantomData)
}
}
impl<const N: usize, C> Status<N, C> {
pub(crate) const LEN: usize = N;
// 获取最高位的完成标记
pub fn get_completed(&self) -> bool {
self.0 >> 31 == 1
@@ -136,20 +150,20 @@ impl<const N: usize> Status<N> {
}
}
impl<const N: usize> From<u32> for Status<N> {
impl<const N: usize, C> From<u32> for Status<N, C> {
fn from(status: u32) -> Self {
Status(status)
Status(status, PhantomData)
}
}
impl<const N: usize> From<Status<N>> for u32 {
fn from(status: Status<N>) -> Self {
impl<const N: usize, C> From<Status<N, C>> for u32 {
fn from(status: Status<N, C>) -> Self {
status.0
}
}
impl<const N: usize> From<Status<N>> for [u32; N] {
fn from(status: Status<N>) -> Self {
impl<const N: usize, C> From<Status<N, C>> for [u32; N] {
fn from(status: Status<N, C>) -> Self {
let mut result = [0; N];
for (i, item) in result.iter_mut().enumerate() {
*item = status.get_status(i);
@@ -158,9 +172,9 @@ impl<const N: usize> From<Status<N>> for [u32; N] {
}
}
impl<const N: usize> From<[u32; N]> for Status<N> {
impl<const N: usize, C> From<[u32; N]> for Status<N, C> {
fn from(status: [u32; N]) -> Self {
let mut result = Status::<N>::default();
let mut result = Self::default();
for (i, item) in status.iter().enumerate() {
assert!(*item < 0b1000, "status should be less than 0b1000");
result.set_status(i, *item);
@@ -173,10 +187,45 @@ impl<const N: usize> From<[u32; N]> for Status<N> {
}
/// 包含五个子任务从前到后依次是视频封面、视频信息、Up 主头像、Up 主信息、分页下载
pub type VideoStatus = Status<5>;
pub type VideoStatus = Status<5, video::Column>;
impl VideoStatus {
pub fn query_builder() -> StatusQueryBuilder<{ Self::LEN }, video::Column> {
StatusQueryBuilder::new(video::Column::DownloadStatus)
}
}
/// 包含五个子任务,从前到后分别是:视频封面、视频内容、视频信息、视频弹幕、视频字幕
pub type PageStatus = Status<5>;
pub type PageStatus = Status<5, page::Column>;
impl PageStatus {
pub fn query_builder() -> StatusQueryBuilder<{ Self::LEN }, page::Column> {
StatusQueryBuilder::new(page::Column::DownloadStatus)
}
}
pub struct StatusQueryBuilder<const N: usize, C: ColumnTrait> {
column: C,
}
impl<const N: usize, C: ColumnTrait> StatusQueryBuilder<N, C> {
fn new(column: C) -> Self {
Self { column }
}
pub fn any_failed(&self) -> Condition {
let mut condition = Condition::any();
for offset in 0..N as i32 {
condition = condition.add(
Expr::col(self.column)
.right_shift(offset * 3)
.bit_and(7)
.is_not_in([0, 7]),
)
}
condition
}
}
#[cfg(test)]
mod tests {
@@ -186,7 +235,7 @@ mod tests {
#[test]
fn test_status_update() {
let mut status = Status::<3>::default();
let mut status = Status::<3, video::Column>::default();
assert_eq!(status.should_run(), [true, true, true]);
for _ in 0..3 {
status.update_status(&[
@@ -217,7 +266,7 @@ mod tests {
fn test_status_convert() {
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
for testcase in testcases.iter() {
let status = Status::<3>::from(testcase.clone());
let status = Status::<3, video::Column>::from(testcase.clone());
assert_eq!(<[u32; 3]>::from(status), *testcase);
}
}
@@ -226,7 +275,7 @@ mod tests {
fn test_status_convert_and_update() {
let testcases = [([0, 0, 1], [1, 7, 7]), ([3, 4, 3], [4, 4, 7]), ([3, 1, 7], [4, 7, 7])];
for (before, after) in testcases.iter() {
let mut status = Status::<3>::from(before.clone());
let mut status = Status::<3, video::Column>::from(before.clone());
status.update_status(&[
ExecutionStatus::Failed(anyhow!("")),
ExecutionStatus::Succeeded,
@@ -239,7 +288,7 @@ mod tests {
#[test]
fn test_status_reset_failed() {
// 重置一个出现部分失败但还有重试次数的任务,将所有的失败状态重置为 0
let mut status = Status::<3>::from([3, 4, 7]);
let mut status = Status::<3, video::Column>::from([3, 4, 7]);
assert!(!status.get_completed());
assert!(status.reset_failed());
assert!(!status.get_completed());
@@ -253,12 +302,12 @@ mod tests {
assert!(status.force_reset_failed());
assert!(!status.get_completed());
// 重置一个已经成功的任务,没有改变状态,也不会修改标记位
let mut status = Status::<3>::from([7, 7, 7]);
let mut status = Status::<3, video::Column>::from([7, 7, 7]);
assert!(status.get_completed());
assert!(!status.reset_failed());
assert!(status.get_completed());
// 重置一个全部失败的任务,修改状态并且修改标记位
let mut status = Status::<3>::from([4, 4, 4]);
let mut status = Status::<3, video::Column>::from([4, 4, 4]);
assert!(status.get_completed());
assert!(status.reset_failed());
assert!(!status.get_completed());
@@ -268,13 +317,13 @@ mod tests {
#[test]
fn test_status_set() {
// 设置子状态,从 completed 到 uncompleted
let mut status = Status::<5>::from([7, 7, 7, 7, 7]);
let mut status = Status::<5, video::Column>::from([7, 7, 7, 7, 7]);
assert!(status.get_completed());
status.set(4, 0);
assert!(!status.get_completed());
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
// 设置子状态,从 uncompleted 到 completed
let mut status = Status::<5>::from([4, 7, 7, 7, 0]);
let mut status = Status::<5, video::Column>::from([4, 7, 7, 7, 0]);
assert!(!status.get_completed());
status.set(4, 7);
assert!(status.get_completed());