From 40d8c2788ab897e168704cc06f3cb2cb8e9cab93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=E1=B4=8D=E1=B4=9B=E1=B4=8F=E1=B4=80=E1=B4=87?= =?UTF-8?q?=CA=80?= Date: Sun, 12 Jul 2026 22:06:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=BA=90=E5=B1=82=E7=BA=A7=E7=9A=84=E6=B5=81=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E8=A7=84=E5=88=99=20(#728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/bili_sync/src/adapter/collection.rs | 4 + crates/bili_sync/src/adapter/favorite.rs | 4 + crates/bili_sync/src/adapter/mod.rs | 2 + crates/bili_sync/src/adapter/submission.rs | 4 + crates/bili_sync/src/adapter/watch_later.rs | 4 + crates/bili_sync/src/api/request.rs | 3 +- crates/bili_sync/src/api/response.rs | 1 + .../src/api/routes/video_sources/mod.rs | 11 ++ .../bili_sync/src/utils/download_context.rs | 5 +- crates/bili_sync/src/workflow.rs | 18 +- .../src/entities/collection.rs | 1 + .../bili_sync_entity/src/entities/favorite.rs | 1 + .../src/entities/submission.rs | 1 + .../src/entities/watch_later.rs | 1 + crates/bili_sync_migration/src/lib.rs | 2 + .../src/m20260712_123205_add_filter_option.rs | 51 +++++ .../components/filter-option-editor.svelte | 183 +++++++++++++++++ web/src/lib/types.ts | 2 + web/src/routes/settings/+page.svelte | 187 +----------------- web/src/routes/video-sources/+page.svelte | 37 +++- 20 files changed, 330 insertions(+), 192 deletions(-) create mode 100644 crates/bili_sync_migration/src/m20260712_123205_add_filter_option.rs create mode 100644 web/src/lib/components/filter-option-editor.svelte diff --git a/crates/bili_sync/src/adapter/collection.rs b/crates/bili_sync/src/adapter/collection.rs index 9a8233a..b8794c7 100644 --- a/crates/bili_sync/src/adapter/collection.rs +++ b/crates/bili_sync/src/adapter/collection.rs @@ -74,6 +74,10 @@ impl VideoSource for collection::Model { &self.rule } + fn filter_option(&self) -> &Option { + &self.filter_option + } + async fn refresh<'a>( self, bili_client: &'a BiliClient, diff --git a/crates/bili_sync/src/adapter/favorite.rs b/crates/bili_sync/src/adapter/favorite.rs index 9669593..d5a9600 100644 --- a/crates/bili_sync/src/adapter/favorite.rs +++ b/crates/bili_sync/src/adapter/favorite.rs @@ -47,6 +47,10 @@ impl VideoSource for favorite::Model { &self.rule } + fn filter_option(&self) -> &Option { + &self.filter_option + } + async fn refresh<'a>( self, bili_client: &'a BiliClient, diff --git a/crates/bili_sync/src/adapter/mod.rs b/crates/bili_sync/src/adapter/mod.rs index a1bc466..c1b2a11 100644 --- a/crates/bili_sync/src/adapter/mod.rs +++ b/crates/bili_sync/src/adapter/mod.rs @@ -77,6 +77,8 @@ pub trait VideoSource { fn rule(&self) -> &Option; + fn filter_option(&self) -> &Option; + fn log_refresh_video_start(&self) { info!("开始扫描{}..", self.display_name()); } diff --git a/crates/bili_sync/src/adapter/submission.rs b/crates/bili_sync/src/adapter/submission.rs index 1ce2a74..0c98f3d 100644 --- a/crates/bili_sync/src/adapter/submission.rs +++ b/crates/bili_sync/src/adapter/submission.rs @@ -82,6 +82,10 @@ impl VideoSource for submission::Model { &self.rule } + fn filter_option(&self) -> &Option { + &self.filter_option + } + async fn refresh<'a>( self, bili_client: &'a BiliClient, diff --git a/crates/bili_sync/src/adapter/watch_later.rs b/crates/bili_sync/src/adapter/watch_later.rs index c123706..a192638 100644 --- a/crates/bili_sync/src/adapter/watch_later.rs +++ b/crates/bili_sync/src/adapter/watch_later.rs @@ -46,6 +46,10 @@ impl VideoSource for watch_later::Model { &self.rule } + fn filter_option(&self) -> &Option { + &self.filter_option + } + async fn refresh<'a>( self, bili_client: &'a BiliClient, diff --git a/crates/bili_sync/src/api/request.rs b/crates/bili_sync/src/api/request.rs index 75ef2ca..df9295d 100644 --- a/crates/bili_sync/src/api/request.rs +++ b/crates/bili_sync/src/api/request.rs @@ -2,7 +2,7 @@ use bili_sync_entity::rule::Rule; use serde::{Deserialize, Serialize}; use validator::Validate; -use crate::bilibili::CollectionType; +use crate::bilibili::{CollectionType, FilterOption}; #[derive(Deserialize)] #[serde(rename_all = "lowercase")] @@ -138,6 +138,7 @@ pub struct UpdateVideoSourceRequest { pub path: String, pub enabled: bool, pub rule: Option, + pub filter_option: Option, pub use_dynamic_api: Option, } diff --git a/crates/bili_sync/src/api/response.rs b/crates/bili_sync/src/api/response.rs index ccf748a..44ee854 100644 --- a/crates/bili_sync/src/api/response.rs +++ b/crates/bili_sync/src/api/response.rs @@ -214,6 +214,7 @@ pub struct VideoSourceDetail { pub name: String, pub path: String, pub rule: Option, + pub filter_option: Option, #[serde(default)] pub rule_display: Option, #[serde(default)] diff --git a/crates/bili_sync/src/api/routes/video_sources/mod.rs b/crates/bili_sync/src/api/routes/video_sources/mod.rs index a6e34e7..0d02040 100644 --- a/crates/bili_sync/src/api/routes/video_sources/mod.rs +++ b/crates/bili_sync/src/api/routes/video_sources/mod.rs @@ -104,6 +104,7 @@ pub async fn get_video_sources_details( collection::Column::Name, collection::Column::Path, collection::Column::Rule, + collection::Column::FilterOption, collection::Column::Enabled, collection::Column::LatestRowAt ]) @@ -116,6 +117,7 @@ pub async fn get_video_sources_details( favorite::Column::Name, favorite::Column::Path, favorite::Column::Rule, + favorite::Column::FilterOption, favorite::Column::Enabled, favorite::Column::LatestRowAt ]) @@ -129,6 +131,7 @@ pub async fn get_video_sources_details( submission::Column::Path, submission::Column::Enabled, submission::Column::Rule, + submission::Column::FilterOption, submission::Column::UseDynamicApi, submission::Column::LatestRowAt ]) @@ -142,6 +145,7 @@ pub async fn get_video_sources_details( watch_later::Column::Path, watch_later::Column::Enabled, watch_later::Column::Rule, + watch_later::Column::FilterOption, watch_later::Column::LatestRowAt ]) .into_model::() @@ -153,6 +157,7 @@ pub async fn get_video_sources_details( name: "稍后再看".to_string(), path: String::new(), rule: None, + filter_option: None, rule_display: None, use_dynamic_api: None, enabled: false, @@ -198,12 +203,14 @@ pub async fn update_video_source( ValidatedJson(request): ValidatedJson, ) -> Result, ApiError> { let rule_display = request.rule.as_ref().map(|rule| rule.to_string()); + let filter_option = request.filter_option.map(serde_json::to_value).transpose()?; let active_model = match source_type.as_str() { "collections" => collection::Entity::find_by_id(id).one(&db).await?.map(|model| { let mut active_model: collection::ActiveModel = model.into(); active_model.path = Set(request.path); active_model.enabled = Set(request.enabled); active_model.rule = Set(request.rule); + active_model.filter_option = Set(filter_option); _ActiveModel::Collection(active_model) }), "favorites" => favorite::Entity::find_by_id(id).one(&db).await?.map(|model| { @@ -211,6 +218,7 @@ pub async fn update_video_source( active_model.path = Set(request.path); active_model.enabled = Set(request.enabled); active_model.rule = Set(request.rule); + active_model.filter_option = Set(filter_option); _ActiveModel::Favorite(active_model) }), "submissions" => submission::Entity::find_by_id(id).one(&db).await?.map(|model| { @@ -218,6 +226,7 @@ pub async fn update_video_source( active_model.path = Set(request.path); active_model.enabled = Set(request.enabled); active_model.rule = Set(request.rule); + active_model.filter_option = Set(filter_option); if let Some(use_dynamic_api) = request.use_dynamic_api { active_model.use_dynamic_api = Set(use_dynamic_api); } @@ -232,6 +241,7 @@ pub async fn update_video_source( active_model.path = Set(request.path); active_model.enabled = Set(request.enabled); active_model.rule = Set(request.rule); + active_model.filter_option = Set(filter_option); Some(_ActiveModel::WatchLater(active_model)) } None => { @@ -243,6 +253,7 @@ pub async fn update_video_source( path: Set(request.path), enabled: Set(request.enabled), rule: Set(request.rule), + filter_option: Set(filter_option), ..Default::default() })) } diff --git a/crates/bili_sync/src/utils/download_context.rs b/crates/bili_sync/src/utils/download_context.rs index 9d9d5e8..ab3941a 100644 --- a/crates/bili_sync/src/utils/download_context.rs +++ b/crates/bili_sync/src/utils/download_context.rs @@ -1,7 +1,7 @@ use sea_orm::DatabaseConnection; use crate::adapter::VideoSourceEnum; -use crate::bilibili::BiliClient; +use crate::bilibili::{BiliClient, FilterOption}; use crate::config::Config; use crate::downloader::Downloader; @@ -13,6 +13,7 @@ pub struct DownloadContext<'a> { pub connection: &'a DatabaseConnection, pub downloader: &'a Downloader, pub config: &'a Config, + pub filter_option: &'a FilterOption, } impl<'a> DownloadContext<'a> { @@ -23,6 +24,7 @@ impl<'a> DownloadContext<'a> { connection: &'a DatabaseConnection, downloader: &'a Downloader, config: &'a Config, + filter_option: &'a FilterOption, ) -> Self { Self { bili_client, @@ -31,6 +33,7 @@ impl<'a> DownloadContext<'a> { connection, downloader, config, + filter_option, } } } diff --git a/crates/bili_sync/src/workflow.rs b/crates/bili_sync/src/workflow.rs index 7db083d..dd22d83 100644 --- a/crates/bili_sync/src/workflow.rs +++ b/crates/bili_sync/src/workflow.rs @@ -201,7 +201,21 @@ pub async fn download_unprocessed_videos( ) -> Result { video_source.log_download_video_start(); let downloader = Downloader::new(bili_client.client.clone()); - let cx = DownloadContext::new(bili_client, video_source, template, connection, &downloader, config); + let source_filter_option = video_source + .filter_option() + .as_ref() + .map(|value| serde_json::from_value(value.clone())) + .transpose()?; + let filter_option = source_filter_option.as_ref().unwrap_or(&config.filter_option); + let cx = DownloadContext::new( + bili_client, + video_source, + template, + connection, + &downloader, + config, + filter_option, + ); let unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?; let mut assigned_upper_ids = HashSet::new(); let tasks = stream::iter(unhandled_videos_pages) @@ -617,7 +631,7 @@ pub async fn fetch_page_video( let streams = bili_video .get_page_analyzer(page_info) .await? - .best_stream(&cx.config.filter_option)?; + .best_stream(cx.filter_option)?; match streams { BestStream::Mixed(mix_stream) => { cx.downloader diff --git a/crates/bili_sync_entity/src/entities/collection.rs b/crates/bili_sync_entity/src/entities/collection.rs index 43d369e..cd360e3 100644 --- a/crates/bili_sync_entity/src/entities/collection.rs +++ b/crates/bili_sync_entity/src/entities/collection.rs @@ -17,6 +17,7 @@ pub struct Model { pub created_at: String, pub latest_row_at: DateTime, pub rule: Option, + pub filter_option: Option, pub enabled: bool, } diff --git a/crates/bili_sync_entity/src/entities/favorite.rs b/crates/bili_sync_entity/src/entities/favorite.rs index f8db60c..9bbd2e7 100644 --- a/crates/bili_sync_entity/src/entities/favorite.rs +++ b/crates/bili_sync_entity/src/entities/favorite.rs @@ -16,6 +16,7 @@ pub struct Model { pub created_at: String, pub latest_row_at: DateTime, pub rule: Option, + pub filter_option: Option, pub enabled: bool, } diff --git a/crates/bili_sync_entity/src/entities/submission.rs b/crates/bili_sync_entity/src/entities/submission.rs index cbb846d..16e5b0d 100644 --- a/crates/bili_sync_entity/src/entities/submission.rs +++ b/crates/bili_sync_entity/src/entities/submission.rs @@ -16,6 +16,7 @@ pub struct Model { pub use_dynamic_api: bool, pub latest_row_at: DateTime, pub rule: Option, + pub filter_option: Option, pub enabled: bool, } diff --git a/crates/bili_sync_entity/src/entities/watch_later.rs b/crates/bili_sync_entity/src/entities/watch_later.rs index 35b669c..f9b0b30 100644 --- a/crates/bili_sync_entity/src/entities/watch_later.rs +++ b/crates/bili_sync_entity/src/entities/watch_later.rs @@ -13,6 +13,7 @@ pub struct Model { pub created_at: String, pub latest_row_at: DateTime, pub rule: Option, + pub filter_option: Option, pub enabled: bool, } diff --git a/crates/bili_sync_migration/src/lib.rs b/crates/bili_sync_migration/src/lib.rs index 44c3a9c..8d80324 100644 --- a/crates/bili_sync_migration/src/lib.rs +++ b/crates/bili_sync_migration/src/lib.rs @@ -11,6 +11,7 @@ mod m20250712_080013_add_video_created_at_index; mod m20250903_094454_add_rule_and_should_download; mod m20251009_123713_add_use_dynamic_api; mod m20260324_055217_add_staff; +mod m20260712_123205_add_filter_option; pub struct Migrator; @@ -29,6 +30,7 @@ impl MigratorTrait for Migrator { Box::new(m20250903_094454_add_rule_and_should_download::Migration), Box::new(m20251009_123713_add_use_dynamic_api::Migration), Box::new(m20260324_055217_add_staff::Migration), + Box::new(m20260712_123205_add_filter_option::Migration), ] } } diff --git a/crates/bili_sync_migration/src/m20260712_123205_add_filter_option.rs b/crates/bili_sync_migration/src/m20260712_123205_add_filter_option.rs new file mode 100644 index 0000000..eaeee3a --- /dev/null +++ b/crates/bili_sync_migration/src/m20260712_123205_add_filter_option.rs @@ -0,0 +1,51 @@ +use sea_orm_migration::prelude::*; +use sea_orm_migration::schema::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + for table in VideoSource::tables() { + manager + .alter_table( + Table::alter() + .table(table) + .add_column(json_null(VideoSource::FilterOption)) + .to_owned(), + ) + .await?; + } + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + for table in VideoSource::tables() { + manager + .alter_table( + Table::alter() + .table(table) + .drop_column(VideoSource::FilterOption) + .to_owned(), + ) + .await?; + } + Ok(()) + } +} + +#[derive(DeriveIden)] +enum VideoSource { + WatchLater, + Submission, + Favorite, + Collection, + FilterOption, +} + +impl VideoSource { + fn tables() -> [Self; 4] { + [Self::WatchLater, Self::Submission, Self::Favorite, Self::Collection] + } +} diff --git a/web/src/lib/components/filter-option-editor.svelte b/web/src/lib/components/filter-option-editor.svelte new file mode 100644 index 0000000..5b7df12 --- /dev/null +++ b/web/src/lib/components/filter-option-editor.svelte @@ -0,0 +1,183 @@ + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+ +

排在前面的编码格式优先级更高

+
+ {#each value.codecs as codec, index (index)} +
+ {index + 1} + {codec} +
+ + + +
+
+ {/each} + + {#if value.codecs.length < 3} +
+ +
+ {#each ['AV1', 'HEV', 'AVC'] as codec (codec)} + {#if !value.codecs.includes(codec)} + + {/if} + {/each} +
+
+ {/if} +
+
+ + + +
+ +

排除某些类型的特殊流

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 477dd16..c6ad4cb 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -222,6 +222,7 @@ export interface VideoSourceDetail { path: string; rule: Rule | null; ruleDisplay: string | null; + filterOption: FilterOption | null; useDynamicApi: boolean | null; enabled: boolean; latestRowAt: string | null; @@ -238,6 +239,7 @@ export interface UpdateVideoSourceRequest { path: string; enabled: boolean; rule?: Rule | null; + filterOption?: FilterOption | null; useDynamicApi?: boolean | null; } diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index bb343a5..1596aea 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -11,6 +11,7 @@ import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import PasswordInput from '$lib/components/custom/password-input.svelte'; import QrLogin from '$lib/components/custom/qr-login.svelte'; + import FilterOptionEditor from '$lib/components/filter-option-editor.svelte'; import NotifierDialog from './NotifierDialog.svelte'; import { InfoIcon, QrCodeIcon } from '@lucide/svelte/icons'; import api from '$lib/api'; @@ -449,191 +450,7 @@ -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - -
- -

排在前面的编码格式优先级更高

-
- {#each formData.filter_option.codecs as codec, index (index)} -
- {index + 1} - {codec} -
- - - -
-
- {/each} - - {#if formData.filter_option.codecs.length < 3} -
- -
- {#each ['AV1', 'HEV', 'AVC'] as codec (codec)} - {#if !formData.filter_option.codecs.includes(codec)} - - {/if} - {/each} -
-
- {/if} -
-
- - - -
- -

排除某些类型的特殊流

-
- - -
-
- - -
-
- - -
-
- - -
-
+ diff --git a/web/src/routes/video-sources/+page.svelte b/web/src/routes/video-sources/+page.svelte index d234c09..b18de5e 100644 --- a/web/src/routes/video-sources/+page.svelte +++ b/web/src/routes/video-sources/+page.svelte @@ -25,15 +25,23 @@ import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import { toast } from 'svelte-sonner'; import { setBreadcrumb } from '$lib/stores/breadcrumb'; - import type { ApiError, VideoSourceDetail, VideoSourcesDetailsResponse, Rule } from '$lib/types'; + import type { + ApiError, + FilterOption, + VideoSourceDetail, + VideoSourcesDetailsResponse, + Rule + } from '$lib/types'; import api from '$lib/api'; 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'; + import FilterOptionEditor from '$lib/components/filter-option-editor.svelte'; let videoSourcesData: VideoSourcesDetailsResponse | null = null; let loading = false; let activeTab = 'favorites'; + let globalFilterOption: FilterOption | null = null; // 添加对话框状态 let showAddDialog = false; @@ -46,6 +54,8 @@ let editingType = ''; let editingIdx: number = 0; let saving = false; + let useCustomFilterOption = false; + let editFilterOption: FilterOption | null = null; // 规则评估对话框状态 let showEvaluateDialog = false; @@ -91,8 +101,12 @@ async function loadVideoSources() { loading = true; try { - const response = await api.getVideoSourcesDetails(); + const [response, configResponse] = await Promise.all([ + api.getVideoSourcesDetails(), + api.getConfig() + ]); videoSourcesData = response.data; + globalFilterOption = configResponse.data.filter_option; } catch (error) { toast.error('加载视频源失败', { description: (error as ApiError).message @@ -113,6 +127,8 @@ useDynamicApi: source.useDynamicApi, rule: source.rule }; + useCustomFilterOption = source.filterOption !== null; + editFilterOption = structuredClone(source.filterOption ?? globalFilterOption!); showEditDialog = true; } @@ -181,7 +197,8 @@ path: editForm.path, enabled: editForm.enabled, rule: editForm.rule, - useDynamicApi: editForm.useDynamicApi + useDynamicApi: editForm.useDynamicApi, + filterOption: useCustomFilterOption ? editFilterOption : null }); // 更新本地数据 if (videoSourcesData && editingSource) { @@ -194,6 +211,7 @@ enabled: editForm.enabled, rule: editForm.rule, useDynamicApi: editForm.useDynamicApi, + filterOption: useCustomFilterOption ? structuredClone(editFilterOption) : null, ruleDisplay: response.data.ruleDisplay }; videoSourcesData = { ...videoSourcesData }; @@ -589,6 +607,19 @@ {/if} +
+
+ +
+ +

关闭时继承全局过滤设置

+
+
+ {#if editFilterOption} + + {/if} +
+
(editForm.rule = rule)} />