mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-09 09:27:13 +08:00
feat: 支持视频源层级的流过滤规则 (#728)
This commit is contained in:
@@ -74,6 +74,10 @@ impl VideoSource for collection::Model {
|
|||||||
&self.rule
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_option(&self) -> &Option<Json> {
|
||||||
|
&self.filter_option
|
||||||
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
self,
|
self,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ impl VideoSource for favorite::Model {
|
|||||||
&self.rule
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_option(&self) -> &Option<Json> {
|
||||||
|
&self.filter_option
|
||||||
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
self,
|
self,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ pub trait VideoSource {
|
|||||||
|
|
||||||
fn rule(&self) -> &Option<Rule>;
|
fn rule(&self) -> &Option<Rule>;
|
||||||
|
|
||||||
|
fn filter_option(&self) -> &Option<Json>;
|
||||||
|
|
||||||
fn log_refresh_video_start(&self) {
|
fn log_refresh_video_start(&self) {
|
||||||
info!("开始扫描{}..", self.display_name());
|
info!("开始扫描{}..", self.display_name());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ impl VideoSource for submission::Model {
|
|||||||
&self.rule
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_option(&self) -> &Option<Json> {
|
||||||
|
&self.filter_option
|
||||||
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
self,
|
self,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ impl VideoSource for watch_later::Model {
|
|||||||
&self.rule
|
&self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_option(&self) -> &Option<Json> {
|
||||||
|
&self.filter_option
|
||||||
|
}
|
||||||
|
|
||||||
async fn refresh<'a>(
|
async fn refresh<'a>(
|
||||||
self,
|
self,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use bili_sync_entity::rule::Rule;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::bilibili::CollectionType;
|
use crate::bilibili::{CollectionType, FilterOption};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@@ -138,6 +138,7 @@ pub struct UpdateVideoSourceRequest {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<FilterOption>,
|
||||||
pub use_dynamic_api: Option<bool>,
|
pub use_dynamic_api: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ pub struct VideoSourceDetail {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<serde_json::Value>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub rule_display: Option<String>,
|
pub rule_display: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ pub async fn get_video_sources_details(
|
|||||||
collection::Column::Name,
|
collection::Column::Name,
|
||||||
collection::Column::Path,
|
collection::Column::Path,
|
||||||
collection::Column::Rule,
|
collection::Column::Rule,
|
||||||
|
collection::Column::FilterOption,
|
||||||
collection::Column::Enabled,
|
collection::Column::Enabled,
|
||||||
collection::Column::LatestRowAt
|
collection::Column::LatestRowAt
|
||||||
])
|
])
|
||||||
@@ -116,6 +117,7 @@ pub async fn get_video_sources_details(
|
|||||||
favorite::Column::Name,
|
favorite::Column::Name,
|
||||||
favorite::Column::Path,
|
favorite::Column::Path,
|
||||||
favorite::Column::Rule,
|
favorite::Column::Rule,
|
||||||
|
favorite::Column::FilterOption,
|
||||||
favorite::Column::Enabled,
|
favorite::Column::Enabled,
|
||||||
favorite::Column::LatestRowAt
|
favorite::Column::LatestRowAt
|
||||||
])
|
])
|
||||||
@@ -129,6 +131,7 @@ pub async fn get_video_sources_details(
|
|||||||
submission::Column::Path,
|
submission::Column::Path,
|
||||||
submission::Column::Enabled,
|
submission::Column::Enabled,
|
||||||
submission::Column::Rule,
|
submission::Column::Rule,
|
||||||
|
submission::Column::FilterOption,
|
||||||
submission::Column::UseDynamicApi,
|
submission::Column::UseDynamicApi,
|
||||||
submission::Column::LatestRowAt
|
submission::Column::LatestRowAt
|
||||||
])
|
])
|
||||||
@@ -142,6 +145,7 @@ pub async fn get_video_sources_details(
|
|||||||
watch_later::Column::Path,
|
watch_later::Column::Path,
|
||||||
watch_later::Column::Enabled,
|
watch_later::Column::Enabled,
|
||||||
watch_later::Column::Rule,
|
watch_later::Column::Rule,
|
||||||
|
watch_later::Column::FilterOption,
|
||||||
watch_later::Column::LatestRowAt
|
watch_later::Column::LatestRowAt
|
||||||
])
|
])
|
||||||
.into_model::<VideoSourceDetail>()
|
.into_model::<VideoSourceDetail>()
|
||||||
@@ -153,6 +157,7 @@ pub async fn get_video_sources_details(
|
|||||||
name: "稍后再看".to_string(),
|
name: "稍后再看".to_string(),
|
||||||
path: String::new(),
|
path: String::new(),
|
||||||
rule: None,
|
rule: None,
|
||||||
|
filter_option: None,
|
||||||
rule_display: None,
|
rule_display: None,
|
||||||
use_dynamic_api: None,
|
use_dynamic_api: None,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -198,12 +203,14 @@ pub async fn update_video_source(
|
|||||||
ValidatedJson(request): ValidatedJson<UpdateVideoSourceRequest>,
|
ValidatedJson(request): ValidatedJson<UpdateVideoSourceRequest>,
|
||||||
) -> Result<ApiResponse<UpdateVideoSourceResponse>, ApiError> {
|
) -> Result<ApiResponse<UpdateVideoSourceResponse>, ApiError> {
|
||||||
let rule_display = request.rule.as_ref().map(|rule| rule.to_string());
|
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() {
|
let active_model = match source_type.as_str() {
|
||||||
"collections" => collection::Entity::find_by_id(id).one(&db).await?.map(|model| {
|
"collections" => collection::Entity::find_by_id(id).one(&db).await?.map(|model| {
|
||||||
let mut active_model: collection::ActiveModel = model.into();
|
let mut active_model: collection::ActiveModel = model.into();
|
||||||
active_model.path = Set(request.path);
|
active_model.path = Set(request.path);
|
||||||
active_model.enabled = Set(request.enabled);
|
active_model.enabled = Set(request.enabled);
|
||||||
active_model.rule = Set(request.rule);
|
active_model.rule = Set(request.rule);
|
||||||
|
active_model.filter_option = Set(filter_option);
|
||||||
_ActiveModel::Collection(active_model)
|
_ActiveModel::Collection(active_model)
|
||||||
}),
|
}),
|
||||||
"favorites" => favorite::Entity::find_by_id(id).one(&db).await?.map(|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.path = Set(request.path);
|
||||||
active_model.enabled = Set(request.enabled);
|
active_model.enabled = Set(request.enabled);
|
||||||
active_model.rule = Set(request.rule);
|
active_model.rule = Set(request.rule);
|
||||||
|
active_model.filter_option = Set(filter_option);
|
||||||
_ActiveModel::Favorite(active_model)
|
_ActiveModel::Favorite(active_model)
|
||||||
}),
|
}),
|
||||||
"submissions" => submission::Entity::find_by_id(id).one(&db).await?.map(|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.path = Set(request.path);
|
||||||
active_model.enabled = Set(request.enabled);
|
active_model.enabled = Set(request.enabled);
|
||||||
active_model.rule = Set(request.rule);
|
active_model.rule = Set(request.rule);
|
||||||
|
active_model.filter_option = Set(filter_option);
|
||||||
if let Some(use_dynamic_api) = request.use_dynamic_api {
|
if let Some(use_dynamic_api) = request.use_dynamic_api {
|
||||||
active_model.use_dynamic_api = Set(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.path = Set(request.path);
|
||||||
active_model.enabled = Set(request.enabled);
|
active_model.enabled = Set(request.enabled);
|
||||||
active_model.rule = Set(request.rule);
|
active_model.rule = Set(request.rule);
|
||||||
|
active_model.filter_option = Set(filter_option);
|
||||||
Some(_ActiveModel::WatchLater(active_model))
|
Some(_ActiveModel::WatchLater(active_model))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -243,6 +253,7 @@ pub async fn update_video_source(
|
|||||||
path: Set(request.path),
|
path: Set(request.path),
|
||||||
enabled: Set(request.enabled),
|
enabled: Set(request.enabled),
|
||||||
rule: Set(request.rule),
|
rule: Set(request.rule),
|
||||||
|
filter_option: Set(filter_option),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use sea_orm::DatabaseConnection;
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
use crate::adapter::VideoSourceEnum;
|
use crate::adapter::VideoSourceEnum;
|
||||||
use crate::bilibili::BiliClient;
|
use crate::bilibili::{BiliClient, FilterOption};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::downloader::Downloader;
|
use crate::downloader::Downloader;
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ pub struct DownloadContext<'a> {
|
|||||||
pub connection: &'a DatabaseConnection,
|
pub connection: &'a DatabaseConnection,
|
||||||
pub downloader: &'a Downloader,
|
pub downloader: &'a Downloader,
|
||||||
pub config: &'a Config,
|
pub config: &'a Config,
|
||||||
|
pub filter_option: &'a FilterOption,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DownloadContext<'a> {
|
impl<'a> DownloadContext<'a> {
|
||||||
@@ -23,6 +24,7 @@ impl<'a> DownloadContext<'a> {
|
|||||||
connection: &'a DatabaseConnection,
|
connection: &'a DatabaseConnection,
|
||||||
downloader: &'a Downloader,
|
downloader: &'a Downloader,
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
|
filter_option: &'a FilterOption,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
bili_client,
|
bili_client,
|
||||||
@@ -31,6 +33,7 @@ impl<'a> DownloadContext<'a> {
|
|||||||
connection,
|
connection,
|
||||||
downloader,
|
downloader,
|
||||||
config,
|
config,
|
||||||
|
filter_option,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,21 @@ pub async fn download_unprocessed_videos(
|
|||||||
) -> Result<DownloadNotifyInfo> {
|
) -> Result<DownloadNotifyInfo> {
|
||||||
video_source.log_download_video_start();
|
video_source.log_download_video_start();
|
||||||
let downloader = Downloader::new(bili_client.client.clone());
|
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 unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?;
|
||||||
let mut assigned_upper_ids = HashSet::new();
|
let mut assigned_upper_ids = HashSet::new();
|
||||||
let tasks = stream::iter(unhandled_videos_pages)
|
let tasks = stream::iter(unhandled_videos_pages)
|
||||||
@@ -617,7 +631,7 @@ pub async fn fetch_page_video(
|
|||||||
let streams = bili_video
|
let streams = bili_video
|
||||||
.get_page_analyzer(page_info)
|
.get_page_analyzer(page_info)
|
||||||
.await?
|
.await?
|
||||||
.best_stream(&cx.config.filter_option)?;
|
.best_stream(cx.filter_option)?;
|
||||||
match streams {
|
match streams {
|
||||||
BestStream::Mixed(mix_stream) => {
|
BestStream::Mixed(mix_stream) => {
|
||||||
cx.downloader
|
cx.downloader
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub struct Model {
|
|||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub latest_row_at: DateTime,
|
pub latest_row_at: DateTime,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<Json>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub struct Model {
|
|||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub latest_row_at: DateTime,
|
pub latest_row_at: DateTime,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<Json>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub struct Model {
|
|||||||
pub use_dynamic_api: bool,
|
pub use_dynamic_api: bool,
|
||||||
pub latest_row_at: DateTime,
|
pub latest_row_at: DateTime,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<Json>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub struct Model {
|
|||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub latest_row_at: DateTime,
|
pub latest_row_at: DateTime,
|
||||||
pub rule: Option<Rule>,
|
pub rule: Option<Rule>,
|
||||||
|
pub filter_option: Option<Json>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod m20250712_080013_add_video_created_at_index;
|
|||||||
mod m20250903_094454_add_rule_and_should_download;
|
mod m20250903_094454_add_rule_and_should_download;
|
||||||
mod m20251009_123713_add_use_dynamic_api;
|
mod m20251009_123713_add_use_dynamic_api;
|
||||||
mod m20260324_055217_add_staff;
|
mod m20260324_055217_add_staff;
|
||||||
|
mod m20260712_123205_add_filter_option;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20250903_094454_add_rule_and_should_download::Migration),
|
Box::new(m20250903_094454_add_rule_and_should_download::Migration),
|
||||||
Box::new(m20251009_123713_add_use_dynamic_api::Migration),
|
Box::new(m20251009_123713_add_use_dynamic_api::Migration),
|
||||||
Box::new(m20260324_055217_add_staff::Migration),
|
Box::new(m20260324_055217_add_staff::Migration),
|
||||||
|
Box::new(m20260712_123205_add_filter_option::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||||
|
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||||
|
import type { FilterOption } from '$lib/types';
|
||||||
|
|
||||||
|
let { value = $bindable(), disabled = false }: { value: FilterOption; disabled?: boolean } =
|
||||||
|
$props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Label>流质量过滤</Label>
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="video-max-quality">最高视频质量</Label>
|
||||||
|
<select
|
||||||
|
id="video-max-quality"
|
||||||
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
bind:value={value.video_max_quality}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<option value="Quality360p">360p</option>
|
||||||
|
<option value="Quality480p">480p</option>
|
||||||
|
<option value="Quality720p">720p</option>
|
||||||
|
<option value="Quality1080p">1080p</option>
|
||||||
|
<option value="Quality1080pPLUS">1080p+</option>
|
||||||
|
<option value="Quality1080p60">1080p60</option>
|
||||||
|
<option value="Quality4k">4K</option>
|
||||||
|
<option value="QualityHdr">HDR</option>
|
||||||
|
<option value="QualityDolby">杜比视界</option>
|
||||||
|
<option value="Quality8k">8K</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="video-min-quality">最低视频质量</Label>
|
||||||
|
<select
|
||||||
|
id="video-min-quality"
|
||||||
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
bind:value={value.video_min_quality}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<option value="Quality360p">360p</option>
|
||||||
|
<option value="Quality480p">480p</option>
|
||||||
|
<option value="Quality720p">720p</option>
|
||||||
|
<option value="Quality1080p">1080p</option>
|
||||||
|
<option value="Quality1080pPLUS">1080p+</option>
|
||||||
|
<option value="Quality1080p60">1080p60</option>
|
||||||
|
<option value="Quality4k">4K</option>
|
||||||
|
<option value="QualityHdr">HDR</option>
|
||||||
|
<option value="QualityDolby">杜比视界</option>
|
||||||
|
<option value="Quality8k">8K</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="audio-max-quality">最高音频质量</Label>
|
||||||
|
<select
|
||||||
|
id="audio-max-quality"
|
||||||
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
bind:value={value.audio_max_quality}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<option value="Quality64k">64k</option>
|
||||||
|
<option value="Quality132k">132k</option>
|
||||||
|
<option value="Quality192k">192k</option>
|
||||||
|
<option value="QualityDolby">杜比全景声</option>
|
||||||
|
<option value="QualityHiRES">Hi-RES</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="audio-min-quality">最低音频质量</Label>
|
||||||
|
<select
|
||||||
|
id="audio-min-quality"
|
||||||
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
bind:value={value.audio_min_quality}
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<option value="Quality64k">64k</option>
|
||||||
|
<option value="Quality132k">132k</option>
|
||||||
|
<option value="Quality192k">192k</option>
|
||||||
|
<option value="QualityDolby">杜比全景声</option>
|
||||||
|
<option value="QualityHiRES">Hi-RES</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Label>视频编码格式偏好(按优先级排序)</Label>
|
||||||
|
<p class="text-muted-foreground text-sm">排在前面的编码格式优先级更高</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each value.codecs as codec, index (index)}
|
||||||
|
<div class="flex items-center space-x-2 rounded-lg border p-3">
|
||||||
|
<Badge variant="secondary">{index + 1}</Badge>
|
||||||
|
<span class="flex-1 font-medium">{codec}</span>
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={disabled || index === 0}
|
||||||
|
onclick={() => {
|
||||||
|
const codecs = [...value.codecs];
|
||||||
|
[codecs[index - 1], codecs[index]] = [codecs[index], codecs[index - 1]];
|
||||||
|
value.codecs = codecs;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={disabled || index === value.codecs.length - 1}
|
||||||
|
onclick={() => {
|
||||||
|
const codecs = [...value.codecs];
|
||||||
|
[codecs[index], codecs[index + 1]] = [codecs[index + 1], codecs[index]];
|
||||||
|
value.codecs = codecs;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
{disabled}
|
||||||
|
onclick={() => (value.codecs = value.codecs.filter((_, i) => i !== index))}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if value.codecs.length < 3}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label>添加编码格式</Label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#each ['AV1', 'HEV', 'AVC'] as codec (codec)}
|
||||||
|
{#if !value.codecs.includes(codec)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
{disabled}
|
||||||
|
onclick={() => (value.codecs = [...value.codecs, codec])}
|
||||||
|
>
|
||||||
|
+ {codec}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<Label>特殊流排除选项</Label>
|
||||||
|
<p class="text-muted-foreground text-sm">排除某些类型的特殊流</p>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch id="no-dolby-video" bind:checked={value.no_dolby_video} {disabled} />
|
||||||
|
<Label for="no-dolby-video">排除杜比视界视频</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch id="no-dolby-audio" bind:checked={value.no_dolby_audio} {disabled} />
|
||||||
|
<Label for="no-dolby-audio">排除杜比全景声音频</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch id="no-hdr" bind:checked={value.no_hdr} {disabled} />
|
||||||
|
<Label for="no-hdr">排除HDR视频</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch id="no-hires" bind:checked={value.no_hires} {disabled} />
|
||||||
|
<Label for="no-hires">排除Hi-RES音频</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -222,6 +222,7 @@ export interface VideoSourceDetail {
|
|||||||
path: string;
|
path: string;
|
||||||
rule: Rule | null;
|
rule: Rule | null;
|
||||||
ruleDisplay: string | null;
|
ruleDisplay: string | null;
|
||||||
|
filterOption: FilterOption | null;
|
||||||
useDynamicApi: boolean | null;
|
useDynamicApi: boolean | null;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
latestRowAt: string | null;
|
latestRowAt: string | null;
|
||||||
@@ -238,6 +239,7 @@ export interface UpdateVideoSourceRequest {
|
|||||||
path: string;
|
path: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
rule?: Rule | null;
|
rule?: Rule | null;
|
||||||
|
filterOption?: FilterOption | null;
|
||||||
useDynamicApi?: boolean | null;
|
useDynamicApi?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import PasswordInput from '$lib/components/custom/password-input.svelte';
|
import PasswordInput from '$lib/components/custom/password-input.svelte';
|
||||||
import QrLogin from '$lib/components/custom/qr-login.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 NotifierDialog from './NotifierDialog.svelte';
|
||||||
import { InfoIcon, QrCodeIcon } from '@lucide/svelte/icons';
|
import { InfoIcon, QrCodeIcon } from '@lucide/svelte/icons';
|
||||||
import api from '$lib/api';
|
import api from '$lib/api';
|
||||||
@@ -449,191 +450,7 @@
|
|||||||
|
|
||||||
<!-- 过滤规则 -->
|
<!-- 过滤规则 -->
|
||||||
<Tabs.Content value="filter" class="mt-6 space-y-6">
|
<Tabs.Content value="filter" class="mt-6 space-y-6">
|
||||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<FilterOptionEditor bind:value={formData.filter_option} />
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="video-max-quality">最高视频质量</Label>
|
|
||||||
<select
|
|
||||||
id="video-max-quality"
|
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
bind:value={formData.filter_option.video_max_quality}
|
|
||||||
>
|
|
||||||
<option value="Quality360p">360p</option>
|
|
||||||
<option value="Quality480p">480p</option>
|
|
||||||
<option value="Quality720p">720p</option>
|
|
||||||
<option value="Quality1080p">1080p</option>
|
|
||||||
<option value="Quality1080pPLUS">1080p+</option>
|
|
||||||
<option value="Quality1080p60">1080p60</option>
|
|
||||||
<option value="Quality4k">4K</option>
|
|
||||||
<option value="QualityHdr">HDR</option>
|
|
||||||
<option value="QualityDolby">杜比视界</option>
|
|
||||||
<option value="Quality8k">8K</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="video-min-quality">最低视频质量</Label>
|
|
||||||
<select
|
|
||||||
id="video-min-quality"
|
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
bind:value={formData.filter_option.video_min_quality}
|
|
||||||
>
|
|
||||||
<option value="Quality360p">360p</option>
|
|
||||||
<option value="Quality480p">480p</option>
|
|
||||||
<option value="Quality720p">720p</option>
|
|
||||||
<option value="Quality1080p">1080p</option>
|
|
||||||
<option value="Quality1080pPLUS">1080p+</option>
|
|
||||||
<option value="Quality1080p60">1080p60</option>
|
|
||||||
<option value="Quality4k">4K</option>
|
|
||||||
<option value="QualityHdr">HDR</option>
|
|
||||||
<option value="QualityDolby">杜比视界</option>
|
|
||||||
<option value="Quality8k">8K</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="audio-max-quality">最高音频质量</Label>
|
|
||||||
<select
|
|
||||||
id="audio-max-quality"
|
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
bind:value={formData.filter_option.audio_max_quality}
|
|
||||||
>
|
|
||||||
<option value="Quality64k">64k</option>
|
|
||||||
<option value="Quality132k">132k</option>
|
|
||||||
<option value="Quality192k">192k</option>
|
|
||||||
<option value="QualityDolby">杜比全景声</option>
|
|
||||||
<option value="QualityHiRES">Hi-RES</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="audio-min-quality">最低音频质量</Label>
|
|
||||||
<select
|
|
||||||
id="audio-min-quality"
|
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
bind:value={formData.filter_option.audio_min_quality}
|
|
||||||
>
|
|
||||||
<option value="Quality64k">64k</option>
|
|
||||||
<option value="Quality132k">132k</option>
|
|
||||||
<option value="Quality192k">192k</option>
|
|
||||||
<option value="QualityDolby">杜比全景声</option>
|
|
||||||
<option value="QualityHiRES">Hi-RES</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<Label>视频编码格式偏好(按优先级排序)</Label>
|
|
||||||
<p class="text-muted-foreground text-sm">排在前面的编码格式优先级更高</p>
|
|
||||||
<div class="space-y-2">
|
|
||||||
{#each formData.filter_option.codecs as codec, index (index)}
|
|
||||||
<div class="flex items-center space-x-2 rounded-lg border p-3">
|
|
||||||
<Badge variant="secondary">{index + 1}</Badge>
|
|
||||||
<span class="flex-1 font-medium">{codec}</span>
|
|
||||||
<div class="flex space-x-1">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={index === 0}
|
|
||||||
onclick={() => {
|
|
||||||
if (formData && index > 0) {
|
|
||||||
const newCodecs = [...formData.filter_option.codecs];
|
|
||||||
[newCodecs[index - 1], newCodecs[index]] = [
|
|
||||||
newCodecs[index],
|
|
||||||
newCodecs[index - 1]
|
|
||||||
];
|
|
||||||
formData.filter_option.codecs = newCodecs;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
disabled={index === formData.filter_option.codecs.length - 1}
|
|
||||||
onclick={() => {
|
|
||||||
if (formData && index < formData.filter_option.codecs.length - 1) {
|
|
||||||
const newCodecs = [...formData.filter_option.codecs];
|
|
||||||
[newCodecs[index], newCodecs[index + 1]] = [
|
|
||||||
newCodecs[index + 1],
|
|
||||||
newCodecs[index]
|
|
||||||
];
|
|
||||||
formData.filter_option.codecs = newCodecs;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onclick={() => {
|
|
||||||
if (formData) {
|
|
||||||
formData.filter_option.codecs = formData.filter_option.codecs.filter(
|
|
||||||
(_, i) => i !== index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if formData.filter_option.codecs.length < 3}
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label>添加编码格式</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
{#each ['AV1', 'HEV', 'AVC'] as codec (codec)}
|
|
||||||
{#if !formData.filter_option.codecs.includes(codec)}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() => {
|
|
||||||
if (formData) {
|
|
||||||
formData.filter_option.codecs = [
|
|
||||||
...formData.filter_option.codecs,
|
|
||||||
codec
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+ {codec}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<Label>特殊流排除选项</Label>
|
|
||||||
<p class="text-muted-foreground text-sm">排除某些类型的特殊流</p>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Switch id="no-dolby-video" bind:checked={formData.filter_option.no_dolby_video} />
|
|
||||||
<Label for="no-dolby-video">排除杜比视界视频</Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Switch id="no-dolby-audio" bind:checked={formData.filter_option.no_dolby_audio} />
|
|
||||||
<Label for="no-dolby-audio">排除杜比全景声音频</Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Switch id="no-hdr" bind:checked={formData.filter_option.no_hdr} />
|
|
||||||
<Label for="no-hdr">排除HDR视频</Label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Switch id="no-hires" bind:checked={formData.filter_option.no_hires} />
|
|
||||||
<Label for="no-hires">排除Hi-RES音频</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,23 @@
|
|||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
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 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 ListRestartIcon from '@lucide/svelte/icons/list-restart';
|
||||||
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
|
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 videoSourcesData: VideoSourcesDetailsResponse | null = null;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let activeTab = 'favorites';
|
let activeTab = 'favorites';
|
||||||
|
let globalFilterOption: FilterOption | null = null;
|
||||||
|
|
||||||
// 添加对话框状态
|
// 添加对话框状态
|
||||||
let showAddDialog = false;
|
let showAddDialog = false;
|
||||||
@@ -46,6 +54,8 @@
|
|||||||
let editingType = '';
|
let editingType = '';
|
||||||
let editingIdx: number = 0;
|
let editingIdx: number = 0;
|
||||||
let saving = false;
|
let saving = false;
|
||||||
|
let useCustomFilterOption = false;
|
||||||
|
let editFilterOption: FilterOption | null = null;
|
||||||
|
|
||||||
// 规则评估对话框状态
|
// 规则评估对话框状态
|
||||||
let showEvaluateDialog = false;
|
let showEvaluateDialog = false;
|
||||||
@@ -91,8 +101,12 @@
|
|||||||
async function loadVideoSources() {
|
async function loadVideoSources() {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await api.getVideoSourcesDetails();
|
const [response, configResponse] = await Promise.all([
|
||||||
|
api.getVideoSourcesDetails(),
|
||||||
|
api.getConfig()
|
||||||
|
]);
|
||||||
videoSourcesData = response.data;
|
videoSourcesData = response.data;
|
||||||
|
globalFilterOption = configResponse.data.filter_option;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('加载视频源失败', {
|
toast.error('加载视频源失败', {
|
||||||
description: (error as ApiError).message
|
description: (error as ApiError).message
|
||||||
@@ -113,6 +127,8 @@
|
|||||||
useDynamicApi: source.useDynamicApi,
|
useDynamicApi: source.useDynamicApi,
|
||||||
rule: source.rule
|
rule: source.rule
|
||||||
};
|
};
|
||||||
|
useCustomFilterOption = source.filterOption !== null;
|
||||||
|
editFilterOption = structuredClone(source.filterOption ?? globalFilterOption!);
|
||||||
showEditDialog = true;
|
showEditDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +197,8 @@
|
|||||||
path: editForm.path,
|
path: editForm.path,
|
||||||
enabled: editForm.enabled,
|
enabled: editForm.enabled,
|
||||||
rule: editForm.rule,
|
rule: editForm.rule,
|
||||||
useDynamicApi: editForm.useDynamicApi
|
useDynamicApi: editForm.useDynamicApi,
|
||||||
|
filterOption: useCustomFilterOption ? editFilterOption : null
|
||||||
});
|
});
|
||||||
// 更新本地数据
|
// 更新本地数据
|
||||||
if (videoSourcesData && editingSource) {
|
if (videoSourcesData && editingSource) {
|
||||||
@@ -194,6 +211,7 @@
|
|||||||
enabled: editForm.enabled,
|
enabled: editForm.enabled,
|
||||||
rule: editForm.rule,
|
rule: editForm.rule,
|
||||||
useDynamicApi: editForm.useDynamicApi,
|
useDynamicApi: editForm.useDynamicApi,
|
||||||
|
filterOption: useCustomFilterOption ? structuredClone(editFilterOption) : null,
|
||||||
ruleDisplay: response.data.ruleDisplay
|
ruleDisplay: response.data.ruleDisplay
|
||||||
};
|
};
|
||||||
videoSourcesData = { ...videoSourcesData };
|
videoSourcesData = { ...videoSourcesData };
|
||||||
@@ -589,6 +607,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Switch bind:checked={useCustomFilterOption} />
|
||||||
|
<div>
|
||||||
|
<Label class="text-sm font-medium">使用自定义流过滤设置</Label>
|
||||||
|
<p class="text-muted-foreground text-sm">关闭时继承全局过滤设置</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if editFilterOption}
|
||||||
|
<FilterOptionEditor bind:value={editFilterOption} disabled={!useCustomFilterOption} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 规则编辑器 -->
|
<!-- 规则编辑器 -->
|
||||||
<div>
|
<div>
|
||||||
<RuleEditor rule={editForm.rule} onRuleChange={(rule) => (editForm.rule = rule)} />
|
<RuleEditor rule={editForm.rule} onRuleChange={(rule) => (editForm.rule = rule)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user