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:
@@ -11,6 +11,8 @@ pub struct VideosRequest {
|
|||||||
pub submission: Option<i32>,
|
pub submission: Option<i32>,
|
||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub failed_only: bool,
|
||||||
pub page: Option<u64>,
|
pub page: Option<u64>,
|
||||||
pub page_size: Option<u64>,
|
pub page_size: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,8 @@ pub struct ResetFilteredVideoStatusRequest {
|
|||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub failed_only: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +69,8 @@ pub struct UpdateFilteredVideoStatusRequest {
|
|||||||
pub watch_later: Option<i32>,
|
pub watch_later: Option<i32>,
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub failed_only: bool,
|
||||||
|
#[serde(default)]
|
||||||
#[validate(nested)]
|
#[validate(nested)]
|
||||||
pub video_updates: Vec<StatusUpdate>,
|
pub video_updates: Vec<StatusUpdate>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ pub async fn get_videos(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.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 total_count = query.clone().count(&db).await?;
|
||||||
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
||||||
(page, page_size)
|
(page, page_size)
|
||||||
@@ -218,6 +221,9 @@ pub async fn reset_filtered_video_status(
|
|||||||
.or(video::Column::Bvid.contains(query_word)),
|
.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_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let all_pages = page::Entity::find()
|
let all_pages = page::Entity::find()
|
||||||
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
.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)),
|
.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_videos = query.into_partial_model::<SimpleVideoInfo>().all(&db).await?;
|
||||||
let mut all_pages = page::Entity::find()
|
let mut all_pages = page::Entity::find()
|
||||||
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
.filter(page::Column::VideoId.is_in(all_videos.iter().map(|v| v.id)))
|
||||||
|
|||||||
@@ -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;
|
use crate::error::ExecutionStatus;
|
||||||
|
|
||||||
pub static STATUS_NOT_STARTED: u32 = 0b000;
|
pub static STATUS_NOT_STARTED: u32 = 0b000;
|
||||||
@@ -11,10 +18,17 @@ pub static STATUS_COMPLETED: u32 = 1 << 31;
|
|||||||
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
/// 如果子任务执行成功,将状态设置为 0b111,该值定义为 STATUS_OK。
|
||||||
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
/// 子任务达到最大失败次数或者执行成功时,认为该子任务已经完成。
|
||||||
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
/// 当所有子任务都已经完成时,为最高位打上标记 1,表示整个下载任务已经完成。
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Status<const N: usize>(u32);
|
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 {
|
pub fn get_completed(&self) -> bool {
|
||||||
self.0 >> 31 == 1
|
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 {
|
fn from(status: u32) -> Self {
|
||||||
Status(status)
|
Status(status, PhantomData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<Status<N>> for u32 {
|
impl<const N: usize, C> From<Status<N, C>> for u32 {
|
||||||
fn from(status: Status<N>) -> Self {
|
fn from(status: Status<N, C>) -> Self {
|
||||||
status.0
|
status.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<Status<N>> for [u32; N] {
|
impl<const N: usize, C> From<Status<N, C>> for [u32; N] {
|
||||||
fn from(status: Status<N>) -> Self {
|
fn from(status: Status<N, C>) -> Self {
|
||||||
let mut result = [0; N];
|
let mut result = [0; N];
|
||||||
for (i, item) in result.iter_mut().enumerate() {
|
for (i, item) in result.iter_mut().enumerate() {
|
||||||
*item = status.get_status(i);
|
*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 {
|
fn from(status: [u32; N]) -> Self {
|
||||||
let mut result = Status::<N>::default();
|
let mut result = Self::default();
|
||||||
for (i, item) in status.iter().enumerate() {
|
for (i, item) in status.iter().enumerate() {
|
||||||
assert!(*item < 0b1000, "status should be less than 0b1000");
|
assert!(*item < 0b1000, "status should be less than 0b1000");
|
||||||
result.set_status(i, *item);
|
result.set_status(i, *item);
|
||||||
@@ -173,10 +187,45 @@ impl<const N: usize> From<[u32; N]> for Status<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、Up 主头像、Up 主信息、分页下载
|
/// 包含五个子任务,从前到后依次是:视频封面、视频信息、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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -186,7 +235,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_update() {
|
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]);
|
assert_eq!(status.should_run(), [true, true, true]);
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
status.update_status(&[
|
status.update_status(&[
|
||||||
@@ -217,7 +266,7 @@ mod tests {
|
|||||||
fn test_status_convert() {
|
fn test_status_convert() {
|
||||||
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
|
let testcases = [[0, 0, 1], [1, 2, 3], [3, 1, 2], [3, 0, 7]];
|
||||||
for testcase in testcases.iter() {
|
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);
|
assert_eq!(<[u32; 3]>::from(status), *testcase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +275,7 @@ mod tests {
|
|||||||
fn test_status_convert_and_update() {
|
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])];
|
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() {
|
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(&[
|
status.update_status(&[
|
||||||
ExecutionStatus::Failed(anyhow!("")),
|
ExecutionStatus::Failed(anyhow!("")),
|
||||||
ExecutionStatus::Succeeded,
|
ExecutionStatus::Succeeded,
|
||||||
@@ -239,7 +288,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_status_reset_failed() {
|
fn test_status_reset_failed() {
|
||||||
// 重置一个出现部分失败但还有重试次数的任务,将所有的失败状态重置为 0
|
// 重置一个出现部分失败但还有重试次数的任务,将所有的失败状态重置为 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.get_completed());
|
||||||
assert!(status.reset_failed());
|
assert!(status.reset_failed());
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
@@ -253,12 +302,12 @@ mod tests {
|
|||||||
assert!(status.force_reset_failed());
|
assert!(status.force_reset_failed());
|
||||||
assert!(!status.get_completed());
|
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.get_completed());
|
||||||
assert!(!status.reset_failed());
|
assert!(!status.reset_failed());
|
||||||
assert!(status.get_completed());
|
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.get_completed());
|
||||||
assert!(status.reset_failed());
|
assert!(status.reset_failed());
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
@@ -268,13 +317,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_status_set() {
|
fn test_status_set() {
|
||||||
// 设置子状态,从 completed 到 uncompleted
|
// 设置子状态,从 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());
|
assert!(status.get_completed());
|
||||||
status.set(4, 0);
|
status.set(4, 0);
|
||||||
assert!(!status.get_completed());
|
assert!(!status.get_completed());
|
||||||
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
|
assert_eq!(<[u32; 5]>::from(status), [7, 7, 7, 7, 0]);
|
||||||
// 设置子状态,从 uncompleted 到 completed
|
// 设置子状态,从 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());
|
assert!(!status.get_completed());
|
||||||
status.set(4, 7);
|
status.set(4, 7);
|
||||||
assert!(status.get_completed());
|
assert!(status.get_completed());
|
||||||
|
|||||||
@@ -7,19 +7,21 @@ export interface AppState {
|
|||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
failedOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appStateStore = writable<AppState>({
|
export const appStateStore = writable<AppState>({
|
||||||
query: '',
|
query: '',
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
videoSource: null
|
videoSource: null,
|
||||||
|
failedOnly: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ToQuery = (state: AppState): string => {
|
export const ToQuery = (state: AppState): string => {
|
||||||
const { query, videoSource } = state;
|
const { query, videoSource, currentPage, failedOnly } = state;
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (state.currentPage > 0) {
|
if (currentPage > 0) {
|
||||||
params.set('page', String(state.currentPage));
|
params.set('page', String(currentPage));
|
||||||
}
|
}
|
||||||
if (query.trim()) {
|
if (query.trim()) {
|
||||||
params.set('query', query);
|
params.set('query', query);
|
||||||
@@ -27,6 +29,9 @@ export const ToQuery = (state: AppState): string => {
|
|||||||
if (videoSource && videoSource.type && videoSource.id) {
|
if (videoSource && videoSource.type && videoSource.id) {
|
||||||
params.set(videoSource.type, videoSource.id);
|
params.set(videoSource.type, videoSource.id);
|
||||||
}
|
}
|
||||||
|
if (failedOnly) {
|
||||||
|
params.set('failed_only', 'true');
|
||||||
|
}
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
return queryString ? `videos?${queryString}` : 'videos';
|
return queryString ? `videos?${queryString}` : 'videos';
|
||||||
};
|
};
|
||||||
@@ -40,6 +45,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
|
failed_only?: boolean;
|
||||||
} => {
|
} => {
|
||||||
const params: {
|
const params: {
|
||||||
query?: string;
|
query?: string;
|
||||||
@@ -47,6 +53,7 @@ export const ToFilterParams = (
|
|||||||
favorite?: number;
|
favorite?: number;
|
||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
|
failed_only?: boolean;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (state.query.trim()) {
|
if (state.query.trim()) {
|
||||||
@@ -57,13 +64,15 @@ export const ToFilterParams = (
|
|||||||
const { type, id } = state.videoSource;
|
const { type, id } = state.videoSource;
|
||||||
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
|
||||||
}
|
}
|
||||||
|
if (state.failedOnly) {
|
||||||
|
params.failed_only = true;
|
||||||
|
}
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否有活动的筛选条件
|
// 检查是否有活动的筛选条件
|
||||||
export const hasActiveFilters = (state: AppState): boolean => {
|
export const hasActiveFilters = (state: AppState): boolean => {
|
||||||
return !!(state.query.trim() || state.videoSource);
|
return !!(state.query.trim() || state.videoSource || state.failedOnly);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setQuery = (query: string) => {
|
export const setQuery = (query: string) => {
|
||||||
@@ -94,6 +103,13 @@ export const setCurrentPage = (page: number) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setFailedOnly = (failedOnly: boolean) => {
|
||||||
|
appStateStore.update((state) => ({
|
||||||
|
...state,
|
||||||
|
failedOnly
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const resetCurrentPage = () => {
|
export const resetCurrentPage = () => {
|
||||||
appStateStore.update((state) => ({
|
appStateStore.update((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -104,12 +120,14 @@ export const resetCurrentPage = () => {
|
|||||||
export const setAll = (
|
export const setAll = (
|
||||||
query: string,
|
query: string,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
videoSource: { type: string; id: string } | null
|
videoSource: { type: string; id: string } | null,
|
||||||
|
failedOnly: boolean
|
||||||
) => {
|
) => {
|
||||||
appStateStore.set({
|
appStateStore.set({
|
||||||
query,
|
query,
|
||||||
currentPage,
|
currentPage,
|
||||||
videoSource
|
videoSource,
|
||||||
|
failedOnly
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,6 +135,7 @@ export const clearAll = () => {
|
|||||||
appStateStore.set({
|
appStateStore.set({
|
||||||
query: '',
|
query: '',
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
videoSource: null
|
videoSource: null,
|
||||||
|
failedOnly: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface VideosRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
failed_only?: boolean;
|
||||||
page?: number;
|
page?: number;
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
}
|
}
|
||||||
@@ -106,6 +107,8 @@ export interface UpdateFilteredVideoStatusRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
// 仅更新下载失败
|
||||||
|
failed_only?: boolean;
|
||||||
video_updates?: StatusUpdate[];
|
video_updates?: StatusUpdate[];
|
||||||
page_updates?: StatusUpdate[];
|
page_updates?: StatusUpdate[];
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,8 @@ export interface ResetFilteredVideoStatusRequest {
|
|||||||
submission?: number;
|
submission?: number;
|
||||||
watch_later?: number;
|
watch_later?: number;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
// 仅重置下载失败
|
||||||
|
failed_only?: boolean;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
setAll,
|
setAll,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
setQuery,
|
setQuery,
|
||||||
|
setFailedOnly,
|
||||||
ToQuery,
|
ToQuery,
|
||||||
ToFilterParams,
|
ToFilterParams,
|
||||||
hasActiveFilters
|
hasActiveFilters
|
||||||
@@ -61,9 +62,13 @@
|
|||||||
videoSource = { type: source.type, id: value };
|
videoSource = { type: source.type, id: value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 支持从 URL 里还原失败筛选
|
||||||
|
const failedParam = searchParams.get('failed_only');
|
||||||
|
const failedOnly = failedParam === 'true' || failedParam === '1';
|
||||||
return {
|
return {
|
||||||
query: searchParams.get('query') || '',
|
query: searchParams.get('query') || '',
|
||||||
videoSource,
|
videoSource,
|
||||||
|
failedOnly,
|
||||||
pageNum: parseInt(searchParams.get('page') || '0')
|
pageNum: parseInt(searchParams.get('page') || '0')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -71,11 +76,12 @@
|
|||||||
async function loadVideos(
|
async function loadVideos(
|
||||||
query: string,
|
query: string,
|
||||||
pageNum: number = 0,
|
pageNum: number = 0,
|
||||||
filter?: { type: string; id: string } | null
|
filter?: { type: string; id: string } | null,
|
||||||
|
failedOnly: boolean = false
|
||||||
) {
|
) {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const params: Record<string, string | number> = {
|
const params: Record<string, string | number | boolean> = {
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
page_size: pageSize
|
page_size: pageSize
|
||||||
};
|
};
|
||||||
@@ -85,6 +91,7 @@
|
|||||||
if (filter) {
|
if (filter) {
|
||||||
params[filter.type] = parseInt(filter.id);
|
params[filter.type] = parseInt(filter.id);
|
||||||
}
|
}
|
||||||
|
params.failed_only = failedOnly;
|
||||||
const result = await api.getVideos(params);
|
const result = await api.getVideos(params);
|
||||||
videosData = result.data;
|
videosData = result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,9 +110,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
async function handleSearchParamsChange(searchParams: URLSearchParams) {
|
||||||
const { query, videoSource, pageNum } = getApiParams(searchParams);
|
const { query, videoSource, pageNum, failedOnly } = getApiParams(searchParams);
|
||||||
setAll(query, pageNum, videoSource);
|
setAll(query, pageNum, videoSource, failedOnly);
|
||||||
loadVideos(query, pageNum, videoSource);
|
loadVideos(query, pageNum, videoSource, failedOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetVideo(id: number, forceReset: boolean) {
|
async function handleResetVideo(id: number, forceReset: boolean) {
|
||||||
@@ -116,8 +123,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `视频「${data.video.name}」已重置`
|
description: `视频「${data.video.name}」已重置`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||||
} else {
|
} else {
|
||||||
toast.info('重置无效', {
|
toast.info('重置无效', {
|
||||||
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
description: `视频「${data.video.name}」没有失败的状态,无需重置`
|
||||||
@@ -144,8 +151,8 @@
|
|||||||
description: `视频「${data.video.name}」已清空重置`
|
description: `视频「${data.video.name}」已清空重置`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空重置失败:', error);
|
console.error('清空重置失败:', error);
|
||||||
toast.error('清空重置失败', {
|
toast.error('清空重置失败', {
|
||||||
@@ -168,8 +175,8 @@
|
|||||||
toast.success('重置成功', {
|
toast.success('重置成功', {
|
||||||
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
description: `已重置 ${data.resetted_videos_count} 个视频和 ${data.resetted_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有需要重置的视频');
|
toast.info('没有需要重置的视频');
|
||||||
}
|
}
|
||||||
@@ -199,8 +206,8 @@
|
|||||||
toast.success('更新成功', {
|
toast.success('更新成功', {
|
||||||
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
|
||||||
});
|
});
|
||||||
const { query, currentPage, videoSource } = $appStateStore;
|
const { query, currentPage, videoSource, failedOnly } = $appStateStore;
|
||||||
await loadVideos(query, currentPage, videoSource);
|
await loadVideos(query, currentPage, videoSource, failedOnly);
|
||||||
} else {
|
} else {
|
||||||
toast.info('没有视频被更新');
|
toast.info('没有视频被更新');
|
||||||
}
|
}
|
||||||
@@ -234,6 +241,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parts.push(`仅失败视频:${state.failedOnly}`);
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,15 +299,29 @@
|
|||||||
></SearchBar>
|
></SearchBar>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground text-sm">筛选:</span>
|
<span class="text-muted-foreground text-sm">筛选:</span>
|
||||||
|
<div
|
||||||
|
class="bg-secondary text-secondary-foreground inline-flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-medium"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="failed-only"
|
||||||
|
checked={$appStateStore.failedOnly}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
setFailedOnly(value);
|
||||||
|
resetCurrentPage();
|
||||||
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label for="failed-only" class="text-xs">仅失败视频</Label>
|
||||||
|
</div>
|
||||||
<DropdownFilter
|
<DropdownFilter
|
||||||
{filters}
|
{filters}
|
||||||
selectedLabel={$appStateStore.videoSource}
|
selectedLabel={$appStateStore.videoSource}
|
||||||
onSelect={(type, id) => {
|
onSelect={(type, id) => {
|
||||||
setAll('', 0, { type, id });
|
setAll('', 0, { type, id }, $appStateStore.failedOnly);
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
}}
|
}}
|
||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
setAll('', 0, null);
|
setAll('', 0, null, $appStateStore.failedOnly);
|
||||||
goto(`/${ToQuery($appStateStore)}`);
|
goto(`/${ToQuery($appStateStore)}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user