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

@@ -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)} />