mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-08-09 06:24:05 +08:00
feat: 支持视频源层级的流过滤规则 (#728)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
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