mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 07:28:04 +08:00
feat: 支持重新评估历史视频,前端显示视频的规则评估状态 (#465)
This commit is contained in:
@@ -64,8 +64,8 @@ impl VideoSource for collection::Model {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule(&self) -> Option<&Rule> {
|
fn rule(&self) -> &Option<Rule> {
|
||||||
self.rule.as_ref()
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ impl VideoSource for favorite::Model {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule(&self) -> Option<&Rule> {
|
fn rule(&self) -> &Option<Rule> {
|
||||||
self.rule.as_ref()
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ pub trait VideoSource {
|
|||||||
video_info.ok()
|
video_info.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule(&self) -> Option<&Rule>;
|
fn rule(&self) -> &Option<Rule>;
|
||||||
|
|
||||||
fn log_refresh_video_start(&self) {
|
fn log_refresh_video_start(&self) {
|
||||||
info!("开始扫描{}..", self.display_name());
|
info!("开始扫描{}..", self.display_name());
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ impl VideoSource for submission::Model {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule(&self) -> Option<&Rule> {
|
fn rule(&self) -> &Option<Rule> {
|
||||||
self.rule.as_ref()
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ impl VideoSource for watch_later::Model {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule(&self) -> Option<&Rule> {
|
fn rule(&self) -> &Option<Rule> {
|
||||||
self.rule.as_ref()
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ pub struct VideoInfo {
|
|||||||
pub bvid: String,
|
pub bvid: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub upper_name: String,
|
pub upper_name: String,
|
||||||
|
pub should_download: bool,
|
||||||
#[serde(serialize_with = "serde_video_download_status")]
|
#[serde(serialize_with = "serde_video_download_status")]
|
||||||
pub download_status: u32,
|
pub download_status: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ use anyhow::Result;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum::extract::{Extension, Path};
|
use axum::extract::{Extension, Path};
|
||||||
use axum::routing::{get, post, put};
|
use axum::routing::{get, post, put};
|
||||||
|
use bili_sync_entity::rule::Rule;
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
use bili_sync_migration::Expr;
|
use bili_sync_migration::Expr;
|
||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{DatabaseConnection, EntityTrait, QuerySelect};
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QuerySelect, TransactionTrait};
|
||||||
|
|
||||||
use crate::adapter::_ActiveModel;
|
use crate::adapter::_ActiveModel;
|
||||||
use crate::api::error::InnerApiError;
|
use crate::api::error::InnerApiError;
|
||||||
@@ -19,12 +21,14 @@ use crate::api::response::{
|
|||||||
};
|
};
|
||||||
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
||||||
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Submission};
|
use crate::bilibili::{BiliClient, Collection, CollectionItem, FavoriteList, Submission};
|
||||||
|
use crate::utils::rule::FieldEvaluatable;
|
||||||
|
|
||||||
pub(super) fn router() -> Router {
|
pub(super) fn router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/video-sources", get(get_video_sources))
|
.route("/video-sources", get(get_video_sources))
|
||||||
.route("/video-sources/details", get(get_video_sources_details))
|
.route("/video-sources/details", get(get_video_sources_details))
|
||||||
.route("/video-sources/{type}/{id}", put(update_video_source))
|
.route("/video-sources/{type}/{id}", put(update_video_source))
|
||||||
|
.route("/video-sources/{type}/{id}/evaluate", post(evaluate_video_source))
|
||||||
.route("/video-sources/favorites", post(insert_favorite))
|
.route("/video-sources/favorites", post(insert_favorite))
|
||||||
.route("/video-sources/collections", post(insert_collection))
|
.route("/video-sources/collections", post(insert_collection))
|
||||||
.route("/video-sources/submissions", post(insert_submission))
|
.route("/video-sources/submissions", post(insert_submission))
|
||||||
@@ -211,6 +215,83 @@ pub async fn update_video_source(
|
|||||||
Ok(ApiResponse::ok(UpdateVideoSourceResponse { rule_display }))
|
Ok(ApiResponse::ok(UpdateVideoSourceResponse { rule_display }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn evaluate_video_source(
|
||||||
|
Path((source_type, id)): Path<(String, i32)>,
|
||||||
|
Extension(db): Extension<DatabaseConnection>,
|
||||||
|
) -> Result<ApiResponse<bool>, ApiError> {
|
||||||
|
// 找出对应 source 的规则与 video 筛选条件
|
||||||
|
let (rule, filter_condition) = match source_type.as_str() {
|
||||||
|
"collections" => (
|
||||||
|
collection::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.column(collection::Column::Rule)
|
||||||
|
.into_tuple::<Option<Rule>>()
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.and_then(|r| r),
|
||||||
|
video::Column::CollectionId.eq(id),
|
||||||
|
),
|
||||||
|
"favorites" => (
|
||||||
|
favorite::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.column(favorite::Column::Rule)
|
||||||
|
.into_tuple::<Option<Rule>>()
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.and_then(|r| r),
|
||||||
|
video::Column::FavoriteId.eq(id),
|
||||||
|
),
|
||||||
|
"submissions" => (
|
||||||
|
submission::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.column(submission::Column::Rule)
|
||||||
|
.into_tuple::<Option<Rule>>()
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.and_then(|r| r),
|
||||||
|
video::Column::SubmissionId.eq(id),
|
||||||
|
),
|
||||||
|
"watch_later" => (
|
||||||
|
watch_later::Entity::find_by_id(id)
|
||||||
|
.select_only()
|
||||||
|
.column(watch_later::Column::Rule)
|
||||||
|
.into_tuple::<Option<Rule>>()
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.and_then(|r| r),
|
||||||
|
video::Column::WatchLaterId.eq(id),
|
||||||
|
),
|
||||||
|
_ => return Err(InnerApiError::BadRequest("Invalid video source type".to_string()).into()),
|
||||||
|
};
|
||||||
|
let videos: Vec<(video::Model, Vec<page::Model>)> = video::Entity::find()
|
||||||
|
.filter(filter_condition)
|
||||||
|
.find_with_related(page::Entity)
|
||||||
|
.all(&db)
|
||||||
|
.await?;
|
||||||
|
let video_should_download_pairs = videos
|
||||||
|
.into_iter()
|
||||||
|
.map(|(video, pages)| (video.id, rule.evaluate_model(&video, &pages)))
|
||||||
|
.collect::<Vec<(i32, bool)>>();
|
||||||
|
let txn = db.begin().await?;
|
||||||
|
for chunk in video_should_download_pairs.chunks(500) {
|
||||||
|
let sql = format!(
|
||||||
|
"WITH tempdata(id, should_download) AS (VALUES {}) \
|
||||||
|
UPDATE video \
|
||||||
|
SET should_download = tempdata.should_download \
|
||||||
|
FROM tempdata \
|
||||||
|
WHERE video.id = tempdata.id",
|
||||||
|
chunk
|
||||||
|
.iter()
|
||||||
|
.map(|item| format!("({}, {})", item.0, item.1))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
txn.execute_unprepared(&sql).await?;
|
||||||
|
}
|
||||||
|
txn.commit().await?;
|
||||||
|
Ok(ApiResponse::ok(true))
|
||||||
|
}
|
||||||
|
|
||||||
/// 新增收藏夹订阅
|
/// 新增收藏夹订阅
|
||||||
pub async fn insert_favorite(
|
pub async fn insert_favorite(
|
||||||
Extension(db): Extension<DatabaseConnection>,
|
Extension(db): Extension<DatabaseConnection>,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub(crate) trait Evaluatable<T> {
|
|||||||
|
|
||||||
pub(crate) trait FieldEvaluatable {
|
pub(crate) trait FieldEvaluatable {
|
||||||
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool;
|
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool;
|
||||||
|
fn evaluate_model(&self, video: &video::Model, pages: &[page::Model]) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Evaluatable<&str> for Condition<String> {
|
impl Evaluatable<&str> for Condition<String> {
|
||||||
@@ -48,6 +49,7 @@ impl Evaluatable<&NaiveDateTime> for Condition<NaiveDateTime> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FieldEvaluatable for RuleTarget {
|
impl FieldEvaluatable for RuleTarget {
|
||||||
|
/// 修改模型后进行评估,此时能访问的是未保存的 activeModel,就地使用 activeModel 评估
|
||||||
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool {
|
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool {
|
||||||
match self {
|
match self {
|
||||||
RuleTarget::Title(cond) => video.name.try_as_ref().is_some_and(|title| cond.evaluate(&title)),
|
RuleTarget::Title(cond) => video.name.try_as_ref().is_some_and(|title| cond.evaluate(&title)),
|
||||||
@@ -72,12 +74,33 @@ impl FieldEvaluatable for RuleTarget {
|
|||||||
RuleTarget::Not(inner) => !inner.evaluate(video, pages),
|
RuleTarget::Not(inner) => !inner.evaluate(video, pages),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 手动触发对历史视频的评估,拿到的是原始 Model,直接使用
|
||||||
|
fn evaluate_model(&self, video: &video::Model, pages: &[page::Model]) -> bool {
|
||||||
|
match self {
|
||||||
|
RuleTarget::Title(cond) => cond.evaluate(&video.name),
|
||||||
|
// 目前的所有条件都是分别针对全体标签进行 any 评估的,例如 Prefix("a") && Suffix("b") 意味着 any(tag.Prefix("a")) && any(tag.Suffix("b")) 而非 any(tag.Prefix("a") && tag.Suffix("b"))
|
||||||
|
// 这可能不满足用户预期,但应该问题不大,如果真有很多人用复杂标签筛选再单独改
|
||||||
|
RuleTarget::Tags(cond) => video
|
||||||
|
.tags
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|tags| tags.0.iter().any(|tag| cond.evaluate(&tag))),
|
||||||
|
RuleTarget::FavTime(cond) => cond.evaluate(&video.favtime.and_utc().with_timezone(&Local).naive_local()),
|
||||||
|
RuleTarget::PubTime(cond) => cond.evaluate(&video.pubtime.and_utc().with_timezone(&Local).naive_local()),
|
||||||
|
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
||||||
|
RuleTarget::Not(inner) => !inner.evaluate_model(video, pages),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldEvaluatable for AndGroup {
|
impl FieldEvaluatable for AndGroup {
|
||||||
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool {
|
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool {
|
||||||
self.iter().all(|target| target.evaluate(video, pages))
|
self.iter().all(|target| target.evaluate(video, pages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_model(&self, video: &video::Model, pages: &[page::Model]) -> bool {
|
||||||
|
self.iter().all(|target| target.evaluate_model(video, pages))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldEvaluatable for Rule {
|
impl FieldEvaluatable for Rule {
|
||||||
@@ -87,6 +110,24 @@ impl FieldEvaluatable for Rule {
|
|||||||
}
|
}
|
||||||
self.0.iter().any(|group| group.evaluate(video, pages))
|
self.0.iter().any(|group| group.evaluate(video, pages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_model(&self, video: &video::Model, pages: &[page::Model]) -> bool {
|
||||||
|
if self.0.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.0.iter().any(|group| group.evaluate_model(video, pages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对于 Option<Rule> 如果 rule 不存在应该被认为是通过评估
|
||||||
|
impl FieldEvaluatable for Option<Rule> {
|
||||||
|
fn evaluate(&self, video: &video::ActiveModel, pages: &[page::ActiveModel]) -> bool {
|
||||||
|
self.as_ref().is_none_or(|rule| rule.evaluate(video, pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_model(&self, video: &video::Model, pages: &[page::Model]) -> bool {
|
||||||
|
self.as_ref().is_none_or(|rule| rule.evaluate_model(video, pages))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -138,9 +138,7 @@ pub async fn fetch_video_details(
|
|||||||
video_source.set_relation_id(&mut video_active_model);
|
video_source.set_relation_id(&mut video_active_model);
|
||||||
video_active_model.single_page = Set(Some(pages.len() == 1));
|
video_active_model.single_page = Set(Some(pages.len() == 1));
|
||||||
video_active_model.tags = Set(Some(tags.into()));
|
video_active_model.tags = Set(Some(tags.into()));
|
||||||
video_active_model.should_download = Set(video_source
|
video_active_model.should_download = Set(video_source.rule().evaluate(&video_active_model, &pages));
|
||||||
.rule()
|
|
||||||
.is_none_or(|r| r.evaluate(&video_active_model, &pages)));
|
|
||||||
let txn = connection.begin().await?;
|
let txn = connection.begin().await?;
|
||||||
create_pages(pages, &txn).await?;
|
create_pages(pages, &txn).await?;
|
||||||
video_active_model.save(&txn).await?;
|
video_active_model.save(&txn).await?;
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth !important;
|
scroll-behavior: smooth !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ class ApiClient {
|
|||||||
return this.put<UpdateVideoSourceResponse>(`/video-sources/${type}/${id}`, request);
|
return this.put<UpdateVideoSourceResponse>(`/video-sources/${type}/${id}`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async evaluateVideoSourceRules(type: string, id: number): Promise<ApiResponse<boolean>> {
|
||||||
|
return this.post<boolean>(`/video-sources/${type}/${id}/evaluate`, null);
|
||||||
|
}
|
||||||
|
|
||||||
async getConfig(): Promise<ApiResponse<Config>> {
|
async getConfig(): Promise<ApiResponse<Config>> {
|
||||||
return this.get<Config>('/config');
|
return this.get<Config>('/config');
|
||||||
}
|
}
|
||||||
@@ -262,6 +266,8 @@ const api = {
|
|||||||
getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(),
|
getVideoSourcesDetails: () => apiClient.getVideoSourcesDetails(),
|
||||||
updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) =>
|
updateVideoSource: (type: string, id: number, request: UpdateVideoSourceRequest) =>
|
||||||
apiClient.updateVideoSource(type, id, request),
|
apiClient.updateVideoSource(type, id, request),
|
||||||
|
evaluateVideoSourceRules: (type: string, id: number) =>
|
||||||
|
apiClient.evaluateVideoSourceRules(type, id),
|
||||||
getConfig: () => apiClient.getConfig(),
|
getConfig: () => apiClient.getConfig(),
|
||||||
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
||||||
getDashboard: () => apiClient.getDashboard(),
|
getDashboard: () => apiClient.getDashboard(),
|
||||||
|
|||||||
@@ -49,20 +49,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOverallStatus(downloadStatus: number[]): {
|
function getOverallStatus(
|
||||||
|
downloadStatus: number[],
|
||||||
|
shouldDownload: boolean
|
||||||
|
): {
|
||||||
text: string;
|
text: string;
|
||||||
color: 'default' | 'secondary' | 'destructive' | 'outline';
|
style: string;
|
||||||
} {
|
} {
|
||||||
|
if (!shouldDownload) {
|
||||||
|
// 被筛选规则排除,显示为“跳过”
|
||||||
|
return { text: '跳过', style: 'bg-gray-100 text-gray-700' };
|
||||||
|
}
|
||||||
const completed = downloadStatus.filter((status) => status === 7).length;
|
const completed = downloadStatus.filter((status) => status === 7).length;
|
||||||
const total = downloadStatus.length;
|
const total = downloadStatus.length;
|
||||||
const failed = downloadStatus.filter((status) => status !== 7 && status !== 0).length;
|
const failed = downloadStatus.filter((status) => status !== 7 && status !== 0).length;
|
||||||
|
|
||||||
if (completed === total) {
|
if (completed === total) {
|
||||||
return { text: '完成', color: 'outline' };
|
// 全部完成,显示为“完成”
|
||||||
|
return { text: '完成', style: 'bg-emerald-700 text-emerald-100' };
|
||||||
} else if (failed > 0) {
|
} else if (failed > 0) {
|
||||||
return { text: '失败', color: 'destructive' };
|
// 出现了失败,显示为“失败”
|
||||||
|
return { text: '失败', style: 'bg-rose-700 text-rose-100' };
|
||||||
} else {
|
} else {
|
||||||
return { text: '进行中', color: 'secondary' };
|
// 还未开始,显示为“等待”
|
||||||
|
return { text: '等待', style: 'bg-yellow-700 text-yellow-100' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +84,7 @@
|
|||||||
return defaultTaskNames[index] || `任务${index + 1}`;
|
return defaultTaskNames[index] || `任务${index + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: overallStatus = getOverallStatus(video.download_status);
|
$: overallStatus = getOverallStatus(video.download_status, video.should_download);
|
||||||
$: completed = video.download_status.filter((status) => status === 7).length;
|
$: completed = video.download_status.filter((status) => status === 7).length;
|
||||||
$: total = video.download_status.length;
|
$: total = video.download_status.length;
|
||||||
|
|
||||||
@@ -112,7 +122,10 @@
|
|||||||
>
|
>
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Badge variant={overallStatus.color} class="shrink-0 px-2 py-1 text-xs font-medium">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="shrink-0 px-2 py-1 text-xs font-medium {overallStatus.style} "
|
||||||
|
>
|
||||||
{overallStatus.text}
|
{overallStatus.text}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface VideoInfo {
|
|||||||
bvid: string;
|
bvid: string;
|
||||||
name: string;
|
name: string;
|
||||||
upper_name: string;
|
upper_name: string;
|
||||||
|
should_download: boolean;
|
||||||
download_status: [number, number, number, number, number];
|
download_status: [number, number, number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
import type { ApiError, VideoSourceDetail, VideoSourcesDetailsResponse, Rule } from '$lib/types';
|
import type { ApiError, VideoSourceDetail, VideoSourcesDetailsResponse, Rule } from '$lib/types';
|
||||||
import api from '$lib/api';
|
import api from '$lib/api';
|
||||||
import RuleEditor from '$lib/components/rule-editor.svelte';
|
import RuleEditor from '$lib/components/rule-editor.svelte';
|
||||||
|
import ListRestartIcon from '@lucide/svelte/icons/list-restart';
|
||||||
|
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
|
||||||
|
|
||||||
let videoSourcesData: VideoSourcesDetailsResponse | null = null;
|
let videoSourcesData: VideoSourcesDetailsResponse | null = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@@ -36,6 +38,12 @@
|
|||||||
let editingIdx: number = 0;
|
let editingIdx: number = 0;
|
||||||
let saving = false;
|
let saving = false;
|
||||||
|
|
||||||
|
// 规则评估对话框状态
|
||||||
|
let showEvaluateDialog = false;
|
||||||
|
let evaluateSource: VideoSourceDetail | null = null;
|
||||||
|
let evaluateType = '';
|
||||||
|
let evaluating = false;
|
||||||
|
|
||||||
// 编辑表单数据
|
// 编辑表单数据
|
||||||
let editForm = {
|
let editForm = {
|
||||||
path: '',
|
path: '',
|
||||||
@@ -83,6 +91,12 @@
|
|||||||
showEditDialog = true;
|
showEditDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEvaluateRules(type: string, source: VideoSourceDetail) {
|
||||||
|
evaluateSource = source;
|
||||||
|
evaluateType = type;
|
||||||
|
showEvaluateDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 保存编辑
|
// 保存编辑
|
||||||
async function saveEdit() {
|
async function saveEdit() {
|
||||||
if (!editingSource) return;
|
if (!editingSource) return;
|
||||||
@@ -91,7 +105,6 @@
|
|||||||
toast.error('路径不能为空');
|
toast.error('路径不能为空');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
let response = await api.updateVideoSource(editingType, editingSource.id, {
|
let response = await api.updateVideoSource(editingType, editingSource.id, {
|
||||||
@@ -99,7 +112,6 @@
|
|||||||
enabled: editForm.enabled,
|
enabled: editForm.enabled,
|
||||||
rule: editForm.rule
|
rule: editForm.rule
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新本地数据
|
// 更新本地数据
|
||||||
if (videoSourcesData && editingSource) {
|
if (videoSourcesData && editingSource) {
|
||||||
const sources = videoSourcesData[
|
const sources = videoSourcesData[
|
||||||
@@ -114,7 +126,6 @@
|
|||||||
};
|
};
|
||||||
videoSourcesData = { ...videoSourcesData };
|
videoSourcesData = { ...videoSourcesData };
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditDialog = false;
|
showEditDialog = false;
|
||||||
toast.success('保存成功');
|
toast.success('保存成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -126,6 +137,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function evaluateRules() {
|
||||||
|
if (!evaluateSource) return;
|
||||||
|
evaluating = true;
|
||||||
|
try {
|
||||||
|
let response = await api.evaluateVideoSourceRules(evaluateType, evaluateSource.id);
|
||||||
|
if (response && response.data) {
|
||||||
|
showEvaluateDialog = false;
|
||||||
|
toast.success('重新评估规则成功');
|
||||||
|
} else {
|
||||||
|
toast.error('重新评估规则失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('重新评估规则失败', {
|
||||||
|
description: (error as ApiError).message
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
evaluating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSourcesForTab(tabValue: string): VideoSourceDetail[] {
|
function getSourcesForTab(tabValue: string): VideoSourceDetail[] {
|
||||||
if (!videoSourcesData) return [];
|
if (!videoSourcesData) return [];
|
||||||
return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[];
|
return videoSourcesData[tabValue as keyof VideoSourcesDetailsResponse] as VideoSourceDetail[];
|
||||||
@@ -288,6 +319,15 @@
|
|||||||
>
|
>
|
||||||
<EditIcon class="h-3 w-3" />
|
<EditIcon class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => openEvaluateRules(key, source)}
|
||||||
|
class="h-8 w-8 p-0"
|
||||||
|
title="重新评估规则"
|
||||||
|
>
|
||||||
|
<ListRestartIcon class="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -330,7 +370,9 @@
|
|||||||
|
|
||||||
<!-- 编辑对话框 -->
|
<!-- 编辑对话框 -->
|
||||||
<Dialog.Root bind:open={showEditDialog}>
|
<Dialog.Root bind:open={showEditDialog}>
|
||||||
<Dialog.Content class="max-h-[85vh] w-4xl !max-w-none overflow-y-auto">
|
<Dialog.Content
|
||||||
|
class="no-scrollbar max-h-[85vh] !max-w-[90vw] overflow-y-auto lg:!max-w-[70vw]"
|
||||||
|
>
|
||||||
<Dialog.Title class="text-lg font-semibold">
|
<Dialog.Title class="text-lg font-semibold">
|
||||||
编辑视频源: {editingSource?.name || ''}
|
编辑视频源: {editingSource?.name || ''}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
@@ -369,6 +411,31 @@
|
|||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<AlertDialog.Root bind:open={showEvaluateDialog}>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>重新评估规则</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
确定要重新评估视频源 <strong>"{evaluateSource?.name}"</strong> 的筛选规则吗?<br />
|
||||||
|
规则修改后默认仅对新视频生效,该操作可使用当前规则对数据库中已存在的历史视频进行重新评估,<span
|
||||||
|
class="text-destructive font-medium">无法撤销</span
|
||||||
|
>。<br />
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel
|
||||||
|
disabled={evaluating}
|
||||||
|
onclick={() => {
|
||||||
|
showEvaluateDialog = false;
|
||||||
|
}}>取消</AlertDialog.Cancel
|
||||||
|
>
|
||||||
|
<AlertDialog.Action onclick={evaluateRules} disabled={evaluating}>
|
||||||
|
{evaluating ? '重新评估中' : '确认重新评估'}
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<!-- 添加对话框 -->
|
<!-- 添加对话框 -->
|
||||||
<Dialog.Root bind:open={showAddDialog}>
|
<Dialog.Root bind:open={showAddDialog}>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
bvid: videoData.video.bvid,
|
bvid: videoData.video.bvid,
|
||||||
name: videoData.video.name,
|
name: videoData.video.name,
|
||||||
upper_name: videoData.video.upper_name,
|
upper_name: videoData.video.upper_name,
|
||||||
download_status: videoData.video.download_status
|
download_status: videoData.video.download_status,
|
||||||
|
should_download: videoData.video.should_download
|
||||||
}}
|
}}
|
||||||
mode="detail"
|
mode="detail"
|
||||||
showActions={false}
|
showActions={false}
|
||||||
@@ -209,7 +210,8 @@
|
|||||||
id: pageInfo.id,
|
id: pageInfo.id,
|
||||||
name: `P${pageInfo.pid}: ${pageInfo.name}`,
|
name: `P${pageInfo.pid}: ${pageInfo.name}`,
|
||||||
upper_name: '',
|
upper_name: '',
|
||||||
download_status: pageInfo.download_status
|
download_status: pageInfo.download_status,
|
||||||
|
should_download: videoData.video.should_download
|
||||||
}}
|
}}
|
||||||
mode="page"
|
mode="page"
|
||||||
showActions={false}
|
showActions={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user