mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-08-06 21:03:37 +08:00
feat: 支持视频源层级的流过滤规则 (#728)
This commit is contained in:
183
web/src/lib/components/filter-option-editor.svelte
Normal file
183
web/src/lib/components/filter-option-editor.svelte
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
Reference in New Issue
Block a user