feat: 支持视频源层级的流过滤规则 (#728)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2026-07-12 22:06:24 +08:00
committed by GitHub
parent 83b07087fb
commit 40d8c2788a
20 changed files with 330 additions and 192 deletions

View File

@@ -74,6 +74,10 @@ impl VideoSource for collection::Model {
&self.rule
}
fn filter_option(&self) -> &Option<Json> {
&self.filter_option
}
async fn refresh<'a>(
self,
bili_client: &'a BiliClient,

View File

@@ -47,6 +47,10 @@ impl VideoSource for favorite::Model {
&self.rule
}
fn filter_option(&self) -> &Option<Json> {
&self.filter_option
}
async fn refresh<'a>(
self,
bili_client: &'a BiliClient,

View File

@@ -77,6 +77,8 @@ pub trait VideoSource {
fn rule(&self) -> &Option<Rule>;
fn filter_option(&self) -> &Option<Json>;
fn log_refresh_video_start(&self) {
info!("开始扫描{}..", self.display_name());
}

View File

@@ -82,6 +82,10 @@ impl VideoSource for submission::Model {
&self.rule
}
fn filter_option(&self) -> &Option<Json> {
&self.filter_option
}
async fn refresh<'a>(
self,
bili_client: &'a BiliClient,

View File

@@ -46,6 +46,10 @@ impl VideoSource for watch_later::Model {
&self.rule
}
fn filter_option(&self) -> &Option<Json> {
&self.filter_option
}
async fn refresh<'a>(
self,
bili_client: &'a BiliClient,

View File

@@ -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<Rule>,
pub filter_option: Option<FilterOption>,
pub use_dynamic_api: Option<bool>,
}

View File

@@ -214,6 +214,7 @@ pub struct VideoSourceDetail {
pub name: String,
pub path: String,
pub rule: Option<Rule>,
pub filter_option: Option<serde_json::Value>,
#[serde(default)]
pub rule_display: Option<String>,
#[serde(default)]

View File

@@ -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::<VideoSourceDetail>()
@@ -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<UpdateVideoSourceRequest>,
) -> Result<ApiResponse<UpdateVideoSourceResponse>, 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()
}))
}

View File

@@ -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,
}
}
}

View File

@@ -201,7 +201,21 @@ pub async fn download_unprocessed_videos(
) -> Result<DownloadNotifyInfo> {
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

View File

@@ -17,6 +17,7 @@ pub struct Model {
pub created_at: String,
pub latest_row_at: DateTime,
pub rule: Option<Rule>,
pub filter_option: Option<Json>,
pub enabled: bool,
}

View File

@@ -16,6 +16,7 @@ pub struct Model {
pub created_at: String,
pub latest_row_at: DateTime,
pub rule: Option<Rule>,
pub filter_option: Option<Json>,
pub enabled: bool,
}

View File

@@ -16,6 +16,7 @@ pub struct Model {
pub use_dynamic_api: bool,
pub latest_row_at: DateTime,
pub rule: Option<Rule>,
pub filter_option: Option<Json>,
pub enabled: bool,
}

View File

@@ -13,6 +13,7 @@ pub struct Model {
pub created_at: String,
pub latest_row_at: DateTime,
pub rule: Option<Rule>,
pub filter_option: Option<Json>,
pub enabled: bool,
}

View File

@@ -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),
]
}
}

View File

@@ -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]
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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 @@
<!-- 过滤规则 -->
<Tabs.Content value="filter" class="mt-6 space-y-6">
<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={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>
<FilterOptionEditor bind:value={formData.filter_option} />
<Separator />

View File

@@ -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 @@
</div>
{/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>
<RuleEditor rule={editForm.rule} onRuleChange={(rule) => (editForm.rule = rule)} />