mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-07-09 06:23:46 +08:00
feat: 过滤规则引入视频总长度和联合投稿 (#684)
This commit is contained in:
@@ -37,13 +37,22 @@ impl Evaluatable<usize> for Condition<usize> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Evaluatable<&NaiveDateTime> for Condition<NaiveDateTime> {
|
impl Evaluatable<NaiveDateTime> for Condition<NaiveDateTime> {
|
||||||
fn evaluate(&self, value: &NaiveDateTime) -> bool {
|
fn evaluate(&self, value: NaiveDateTime) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Condition::Equals(expected) => expected == value,
|
Condition::Equals(expected) => *expected == value,
|
||||||
Condition::GreaterThan(threshold) => value > threshold,
|
Condition::GreaterThan(threshold) => value > *threshold,
|
||||||
Condition::LessThan(threshold) => value < threshold,
|
Condition::LessThan(threshold) => value < *threshold,
|
||||||
Condition::Between(start, end) => value > start && value < end,
|
Condition::Between(start, end) => value > *start && value < *end,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Evaluatable<bool> for Condition<bool> {
|
||||||
|
fn evaluate(&self, value: bool) -> bool {
|
||||||
|
match self {
|
||||||
|
Condition::Equals(expected) => *expected == value,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,13 +74,20 @@ impl FieldEvaluatable for RuleTarget {
|
|||||||
.favtime
|
.favtime
|
||||||
.try_as_ref()
|
.try_as_ref()
|
||||||
.map(|fav_time| fav_time.and_utc().with_timezone(&Local).naive_local()) // 数据库中保存的一律是 utc 时间,转换为 local 时间再比较
|
.map(|fav_time| fav_time.and_utc().with_timezone(&Local).naive_local()) // 数据库中保存的一律是 utc 时间,转换为 local 时间再比较
|
||||||
.is_some_and(|fav_time| cond.evaluate(&fav_time)),
|
.is_some_and(|fav_time| cond.evaluate(fav_time)),
|
||||||
RuleTarget::PubTime(cond) => video
|
RuleTarget::PubTime(cond) => video
|
||||||
.pubtime
|
.pubtime
|
||||||
.try_as_ref()
|
.try_as_ref()
|
||||||
.map(|pub_time| pub_time.and_utc().with_timezone(&Local).naive_local())
|
.map(|pub_time| pub_time.and_utc().with_timezone(&Local).naive_local())
|
||||||
.is_some_and(|pub_time| cond.evaluate(&pub_time)),
|
.is_some_and(|pub_time| cond.evaluate(pub_time)),
|
||||||
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
||||||
|
RuleTarget::SumVideoLength(cond) => pages
|
||||||
|
.iter()
|
||||||
|
.try_fold(0usize, |acc, page| {
|
||||||
|
page.duration.try_as_ref().map(|d| acc + *d as usize).ok_or(())
|
||||||
|
})
|
||||||
|
.is_ok_and(|total_length| cond.evaluate(total_length)),
|
||||||
|
RuleTarget::MultiUpper(cond) => cond.evaluate(video.staff.as_ref().is_some()),
|
||||||
RuleTarget::Not(inner) => !inner.evaluate(video, pages),
|
RuleTarget::Not(inner) => !inner.evaluate(video, pages),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,9 +102,13 @@ impl FieldEvaluatable for RuleTarget {
|
|||||||
.tags
|
.tags
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|tags| tags.0.iter().any(|tag| cond.evaluate(tag))),
|
.is_some_and(|tags| tags.0.iter().any(|tag| cond.evaluate(tag))),
|
||||||
RuleTarget::FavTime(cond) => cond.evaluate(&video.favtime.and_utc().with_timezone(&Local).naive_local()),
|
RuleTarget::FavTime(cond) => cond.evaluate(video.favtime.and_utc().with_timezone(&Local).naive_local()),
|
||||||
RuleTarget::PubTime(cond) => cond.evaluate(&video.pubtime.and_utc().with_timezone(&Local).naive_local()),
|
RuleTarget::PubTime(cond) => cond.evaluate(video.pubtime.and_utc().with_timezone(&Local).naive_local()),
|
||||||
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
RuleTarget::PageCount(cond) => cond.evaluate(pages.len()),
|
||||||
|
RuleTarget::SumVideoLength(cond) => {
|
||||||
|
cond.evaluate(pages.iter().fold(0usize, |acc, page| acc + page.duration as usize))
|
||||||
|
}
|
||||||
|
RuleTarget::MultiUpper(cond) => cond.evaluate(video.staff.is_some()),
|
||||||
RuleTarget::Not(inner) => !inner.evaluate_model(video, pages),
|
RuleTarget::Not(inner) => !inner.evaluate_model(video, pages),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ pub enum RuleTarget {
|
|||||||
FavTime(Condition<DateTime>),
|
FavTime(Condition<DateTime>),
|
||||||
PubTime(Condition<DateTime>),
|
PubTime(Condition<DateTime>),
|
||||||
PageCount(Condition<usize>),
|
PageCount(Condition<usize>),
|
||||||
|
SumVideoLength(Condition<usize>),
|
||||||
|
MultiUpper(Condition<bool>),
|
||||||
Not(Box<RuleTarget>),
|
Not(Box<RuleTarget>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +65,8 @@ impl Display for RuleTarget {
|
|||||||
RuleTarget::FavTime(_) => "收藏时间",
|
RuleTarget::FavTime(_) => "收藏时间",
|
||||||
RuleTarget::PubTime(_) => "发布时间",
|
RuleTarget::PubTime(_) => "发布时间",
|
||||||
RuleTarget::PageCount(_) => "视频分页数量",
|
RuleTarget::PageCount(_) => "视频分页数量",
|
||||||
|
RuleTarget::SumVideoLength(_) => "视频总时长",
|
||||||
|
RuleTarget::MultiUpper(_) => "联合投稿",
|
||||||
RuleTarget::Not(inner) => {
|
RuleTarget::Not(inner) => {
|
||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
get_field_name(inner, depth + 1)
|
get_field_name(inner, depth + 1)
|
||||||
@@ -79,14 +83,16 @@ impl Display for RuleTarget {
|
|||||||
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
||||||
write!(f, "{}不{}", field_name, cond)
|
write!(f, "{}不{}", field_name, cond)
|
||||||
}
|
}
|
||||||
RuleTarget::PageCount(cond) => write!(f, "{}不{}", field_name, cond),
|
RuleTarget::PageCount(cond) | RuleTarget::SumVideoLength(cond) => write!(f, "{}不{}", field_name, cond),
|
||||||
|
RuleTarget::MultiUpper(cond) => write!(f, "{}不{}", field_name, cond),
|
||||||
RuleTarget::Not(_) => write!(f, "格式化失败"),
|
RuleTarget::Not(_) => write!(f, "格式化失败"),
|
||||||
},
|
},
|
||||||
RuleTarget::Title(cond) | RuleTarget::Tags(cond) => write!(f, "{}{}", field_name, cond),
|
RuleTarget::Title(cond) | RuleTarget::Tags(cond) => write!(f, "{}{}", field_name, cond),
|
||||||
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
RuleTarget::FavTime(cond) | RuleTarget::PubTime(cond) => {
|
||||||
write!(f, "{}{}", field_name, cond)
|
write!(f, "{}{}", field_name, cond)
|
||||||
}
|
}
|
||||||
RuleTarget::PageCount(cond) => write!(f, "{}{}", field_name, cond),
|
RuleTarget::PageCount(cond) | RuleTarget::SumVideoLength(cond) => write!(f, "{}{}", field_name, cond),
|
||||||
|
RuleTarget::MultiUpper(cond) => write!(f, "{}{}", field_name, cond),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
|
import * as Select from '$lib/components/ui/select/index.js';
|
||||||
import { PlusIcon, MinusIcon, XIcon } from '@lucide/svelte/icons';
|
import { PlusIcon, MinusIcon, XIcon } from '@lucide/svelte/icons';
|
||||||
import type { Rule, RuleTarget, Condition } from '$lib/types';
|
import type { Rule, RuleTarget, Condition } from '$lib/types';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@@ -21,7 +22,9 @@
|
|||||||
{ value: 'tags', label: '标签' },
|
{ value: 'tags', label: '标签' },
|
||||||
{ value: 'favTime', label: '收藏时间' },
|
{ value: 'favTime', label: '收藏时间' },
|
||||||
{ value: 'pubTime', label: '发布时间' },
|
{ value: 'pubTime', label: '发布时间' },
|
||||||
{ value: 'pageCount', label: '视频分页数量' }
|
{ value: 'pageCount', label: '视频分页数量' },
|
||||||
|
{ value: 'sumVideoLength', label: '视频总时长' },
|
||||||
|
{ value: 'multiUpper', label: '联合投稿' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const getOperatorOptions = (field: string) => {
|
const getOperatorOptions = (field: string) => {
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
{ value: 'matchesRegex', label: '匹配正则' }
|
{ value: 'matchesRegex', label: '匹配正则' }
|
||||||
];
|
];
|
||||||
case 'pageCount':
|
case 'pageCount':
|
||||||
|
case 'sumVideoLength':
|
||||||
return [
|
return [
|
||||||
{ value: 'equals', label: '等于' },
|
{ value: 'equals', label: '等于' },
|
||||||
{ value: 'greaterThan', label: '大于' },
|
{ value: 'greaterThan', label: '大于' },
|
||||||
@@ -51,6 +55,8 @@
|
|||||||
{ value: 'lessThan', label: '早于' },
|
{ value: 'lessThan', label: '早于' },
|
||||||
{ value: 'between', label: '时间范围' }
|
{ value: 'between', label: '时间范围' }
|
||||||
];
|
];
|
||||||
|
case 'multiUpper':
|
||||||
|
return [{ value: 'equals', label: '等于' }];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -80,7 +86,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function convertRuleTargetToLocal(target: RuleTarget<string | number | Date>): LocalCondition {
|
function convertRuleTargetToLocal(
|
||||||
|
target: RuleTarget<string | number | boolean | Date>
|
||||||
|
): LocalCondition {
|
||||||
if (typeof target.rule === 'object' && 'field' in target.rule) {
|
if (typeof target.rule === 'object' && 'field' in target.rule) {
|
||||||
// 嵌套的 not
|
// 嵌套的 not
|
||||||
const innerCondition = convertRuleTargetToLocal(target.rule);
|
const innerCondition = convertRuleTargetToLocal(target.rule);
|
||||||
@@ -93,10 +101,10 @@
|
|||||||
let value = '';
|
let value = '';
|
||||||
let value2 = '';
|
let value2 = '';
|
||||||
if (Array.isArray(condition.value)) {
|
if (Array.isArray(condition.value)) {
|
||||||
value = String(condition.value[0] || '');
|
value = String(condition.value[0] ?? '');
|
||||||
value2 = String(condition.value[1] || '');
|
value2 = String(condition.value[1] ?? '');
|
||||||
} else {
|
} else {
|
||||||
value = String(condition.value || '');
|
value = String(condition.value ?? '');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
field: target.field,
|
field: target.field,
|
||||||
@@ -111,8 +119,8 @@
|
|||||||
if (localRule.length === 0) return null;
|
if (localRule.length === 0) return null;
|
||||||
return localRule.map((andGroup) =>
|
return localRule.map((andGroup) =>
|
||||||
andGroup.conditions.map((condition) => {
|
andGroup.conditions.map((condition) => {
|
||||||
let value: string | number | Date | (string | number | Date)[];
|
let value: string | number | boolean | Date | (string | number | boolean | Date)[];
|
||||||
if (condition.field === 'pageCount') {
|
if (condition.field === 'pageCount' || condition.field === 'sumVideoLength') {
|
||||||
if (condition.operator === 'between') {
|
if (condition.operator === 'between') {
|
||||||
value = [parseInt(condition.value) || 0, parseInt(condition.value2 || '0') || 0];
|
value = [parseInt(condition.value) || 0, parseInt(condition.value2 || '0') || 0];
|
||||||
} else {
|
} else {
|
||||||
@@ -124,6 +132,8 @@
|
|||||||
} else {
|
} else {
|
||||||
value = condition.value;
|
value = condition.value;
|
||||||
}
|
}
|
||||||
|
} else if (condition.field === 'multiUpper') {
|
||||||
|
value = condition.value === 'true';
|
||||||
} else {
|
} else {
|
||||||
if (condition.operator === 'between') {
|
if (condition.operator === 'between') {
|
||||||
value = [condition.value, condition.value2 || ''];
|
value = [condition.value, condition.value2 || ''];
|
||||||
@@ -131,12 +141,12 @@
|
|||||||
value = condition.value;
|
value = condition.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const conditionObj: Condition<string | number | Date> = {
|
const conditionObj: Condition<string | number | boolean | Date> = {
|
||||||
operator: condition.operator,
|
operator: condition.operator,
|
||||||
value
|
value
|
||||||
};
|
};
|
||||||
|
|
||||||
let target: RuleTarget<string | number | Date> = {
|
let target: RuleTarget<string | number | boolean | Date> = {
|
||||||
field: condition.field,
|
field: condition.field,
|
||||||
rule: conditionObj
|
rule: conditionObj
|
||||||
};
|
};
|
||||||
@@ -187,7 +197,7 @@
|
|||||||
condition.field = value;
|
condition.field = value;
|
||||||
const operators = getOperatorOptions(value);
|
const operators = getOperatorOptions(value);
|
||||||
condition.operator = operators[0]?.value || 'equals';
|
condition.operator = operators[0]?.value || 'equals';
|
||||||
condition.value = '';
|
condition.value = value === 'multiUpper' ? 'false' : '';
|
||||||
condition.value2 = '';
|
condition.value2 = '';
|
||||||
} else if (field === 'operator') {
|
} else if (field === 'operator') {
|
||||||
condition.operator = value;
|
condition.operator = value;
|
||||||
@@ -290,36 +300,43 @@
|
|||||||
<!-- 字段选择 -->
|
<!-- 字段选择 -->
|
||||||
<div>
|
<div>
|
||||||
<Label class="text-muted-foreground text-xs">字段</Label>
|
<Label class="text-muted-foreground text-xs">字段</Label>
|
||||||
<select
|
<Select.Root
|
||||||
class="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
type="single"
|
||||||
value={condition.field}
|
value={condition.field}
|
||||||
onchange={(e) =>
|
onValueChange={(v) => updateCondition(groupIndex, conditionIndex, 'field', v)}
|
||||||
updateCondition(groupIndex, conditionIndex, 'field', e.currentTarget.value)}
|
|
||||||
>
|
>
|
||||||
{#each FIELD_OPTIONS as option (option.value)}
|
<Select.Trigger class="w-full">
|
||||||
<option value={option.value}>{option.label}</option>
|
{FIELD_OPTIONS.find((o) => o.value === condition.field)?.label ??
|
||||||
{/each}
|
condition.field}
|
||||||
</select>
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each FIELD_OPTIONS as option (option.value)}
|
||||||
|
<Select.Item value={option.value} label={option.label} />
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作符选择 -->
|
<!-- 操作符选择 -->
|
||||||
<div>
|
<div>
|
||||||
<Label class="text-muted-foreground text-xs">操作符</Label>
|
<Label class="text-muted-foreground text-xs">操作符</Label>
|
||||||
<select
|
<Select.Root
|
||||||
class="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
type="single"
|
||||||
value={condition.operator}
|
value={condition.operator}
|
||||||
onchange={(e) =>
|
onValueChange={(v) =>
|
||||||
updateCondition(
|
updateCondition(groupIndex, conditionIndex, 'operator', v)}
|
||||||
groupIndex,
|
|
||||||
conditionIndex,
|
|
||||||
'operator',
|
|
||||||
e.currentTarget.value
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{#each getOperatorOptions(condition.field) as option (option.value)}
|
<Select.Trigger class="w-full">
|
||||||
<option value={option.value}>{option.label}</option>
|
{getOperatorOptions(condition.field).find(
|
||||||
{/each}
|
(o) => o.value === condition.operator
|
||||||
</select>
|
)?.label ?? condition.operator}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each getOperatorOptions(condition.field) as option (option.value)}
|
||||||
|
<Select.Item value={option.value} label={option.label} />
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -328,7 +345,7 @@
|
|||||||
<Label class="text-muted-foreground text-xs">值</Label>
|
<Label class="text-muted-foreground text-xs">值</Label>
|
||||||
{#if condition.operator === 'between'}
|
{#if condition.operator === 'between'}
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
{#if condition.field === 'pageCount'}
|
{#if condition.field === 'pageCount' || condition.field === 'sumVideoLength'}
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="最小值"
|
placeholder="最小值"
|
||||||
@@ -411,7 +428,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if condition.field === 'pageCount'}
|
{:else if condition.field === 'pageCount' || condition.field === 'sumVideoLength'}
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="输入数值"
|
placeholder="输入数值"
|
||||||
@@ -434,6 +451,20 @@
|
|||||||
e.currentTarget.value + ':00'
|
e.currentTarget.value + ':00'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{:else if condition.field === 'multiUpper'}
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
value={condition.value}
|
||||||
|
onValueChange={(v) => updateCondition(groupIndex, conditionIndex, 'value', v)}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="w-full">
|
||||||
|
{condition.value === 'true' ? 'true' : 'false'}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="true" label="true" />
|
||||||
|
<Select.Item value="false" label="false" />
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export interface RuleTarget<T> {
|
|||||||
rule: Condition<T> | RuleTarget<T>;
|
rule: Condition<T> | RuleTarget<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AndGroup = RuleTarget<string | number | Date>[];
|
export type AndGroup = RuleTarget<string | number | boolean | Date>[];
|
||||||
export type Rule = AndGroup[];
|
export type Rule = AndGroup[];
|
||||||
|
|
||||||
export interface VideoSourceDetail {
|
export interface VideoSourceDetail {
|
||||||
|
|||||||
Reference in New Issue
Block a user