Improved the testing logic for conditions filter.

This commit is contained in:
arabcoders 2025-06-03 22:15:06 +03:00
parent 55b2e3e7f2
commit 38bf6af1fc
5 changed files with 214 additions and 79 deletions

View file

@ -883,32 +883,31 @@ class HttpAPI(Common):
Response: The response object
"""
data = await request.json()
params = await request.json()
if not isinstance(data, dict):
if not isinstance(params, dict):
return web.json_response(
{"error": "Invalid request body expecting dict."},
status=web.HTTPBadRequest.status_code,
)
url = data.get("url")
url = params.get("url")
if not url:
return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code)
cond = data.get("condition")
cond = params.get("condition")
if not cond:
return web.json_response({"error": "condition name is required."}, status=web.HTTPBadRequest.status_code)
return web.json_response({"error": "condition is required."}, status=web.HTTPBadRequest.status_code)
try:
opts = {}
if ytdlp_proxy := self.config.get_ytdlp_args().get("proxy", None):
opts["proxy"] = ytdlp_proxy
preset = data.get("preset", self.config.default_preset)
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all()
preset = params.get("preset", self.config.default_preset)
key = self.cache.hash(url + str(preset))
if not self.cache.has(key):
opts = {}
if ytdlp_proxy := self.config.get_ytdlp_args().get("proxy", None):
opts["proxy"] = ytdlp_proxy
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all()
data = extract_info(
config=ytdlp_opts,
url=url,
@ -917,6 +916,11 @@ class HttpAPI(Common):
follow_redirect=True,
sanitize_info=True,
)
if not data:
return web.json_response(
data={"error": f"Failed to extract info from '{url!s}'."},
status=web.HTTPBadRequest.status_code,
)
self.cache.set(key=key, value=data, ttl=600)
else:
data = self.cache.get(key)
@ -927,16 +931,21 @@ class HttpAPI(Common):
status=web.HTTPInternalServerError.status_code,
)
matched = Conditions.get_instance().single_match(name=cond, info=data)
if matched is None:
try:
from yt_dlp.utils import match_str
status = match_str(cond, data)
except Exception as e:
LOG.exception(e)
return web.json_response(
data={"error": f"Condition '{cond}' doesn't match the data from the URL."},
status=web.HTTPNotFound.status_code,
data={"error": str(e)},
status=web.HTTPBadRequest.status_code,
)
return web.json_response(
data={"message": f"Condition '{cond}' matched the data from the URL."},
data={"status": status, "condition": cond, "data": data},
status=web.HTTPOk.status_code,
dumps=self.encoder.encode,
)
@route("GET", "api/presets")

View file

@ -70,7 +70,7 @@
<div class="field">
<label class="label is-inline" for="filter">
<span class="icon"><i class="fa-solid fa-filter" /></span>
Filter
Condition Filter
</label>
<div class="control">
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress"
@ -78,7 +78,12 @@
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The yt-dlp <code>[--match-filters]</code> filter logic.</span>
<span>
The yt-dlp <code>[--match-filters]</code> filter logic.
<NuxtLink @click="test_data.show = true" :disabled="addInProgress || !form.filter" class="is-bold">
Test filter logic
</NuxtLink>
</span>
</span>
</div>
</div>
@ -108,29 +113,102 @@
<div class="card-footer mt-auto">
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="addForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
<div class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
:class="{ 'is-loading': addInProgress }" form="addForm">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Save</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
type="button">
<span class="icon"><i class="fa-solid fa-times" /></span>
<span>Cancel</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<div v-if="test_data.show" class="column is-12">
<Modal @close="test_data.show = false" title="Test condition">
<form autocomplete="off" id="testCondition" @submit.prevent="run_test()">
<div class="card">
<div class="card-content">
<div class="field">
<label class="label is-inline" for="url">
<span class="icon"><i class="fa-solid fa-link" /></span>
URL
</label>
<div class="field-body">
<div class="field is-grouped">
<div class="control is-expanded">
<input type="url" class="input " id="url" v-model="test_data.url"
:disabled="test_data.in_progress" placeholder="https://..." required>
</div>
<div class="control">
<button class="button is-primary" type="submit" :disabled="test_data.in_progress"
:class="{ 'is-loading': test_data.in_progress }">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Test</span>
</button>
</div>
</div>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The url to test the filter against.</span>
</span>
</div>
<div class="field">
<label class="label is-inline" for="filter">
<span class="icon"><i class="fa-solid fa-filter" /></span>
Condition Filter
</label>
<div class="control">
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="test_data.in_progress"
placeholder="availability = 'needs_auth' & channel_id = 'channel_id'" required>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The yt-dlp <code>[--match-filters]</code> filter logic.</span><br>
</span>
</div>
<div class="field">
<span class="is-bold" :class="{
'has-text-success': true === test_data?.data?.status,
'has-text-danger': false === test_data?.data?.status,
}">
<span class="icon"><i class="fa-solid" :class="{
'fa-check': true === test_data.data.status,
'fa-xmark': false === test_data.data.status,
'fa-question': null === (test_data.data.status || null),
}" /></span>
Filter Status: {{ test_data?.data?.status === null ? 'Not tested' : test_data?.data?.status ?
'Matched' : 'Not matched' }}
</span>
</div>
<div class="field">
<pre style="height:60vh;"><code>{{ show_data() }}</code></pre>
</div>
</div>
</div>
</form>
</Modal>
</div>
</main>
</template>
<script setup>
import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']);
const emitter = defineEmits(['cancel', 'submit'])
const props = defineProps({
reference: {
@ -149,31 +227,72 @@ const props = defineProps({
},
})
const test_data = ref({ show: false, url: '', data: { status: null, data: {} }, in_progress: false })
const toast = useNotification()
const box = useConfirm()
const form = reactive(JSON.parse(JSON.stringify(props.item)))
const import_string = ref('')
const showImport = useStorage('showImport', false);
const showImport = useStorage('showImport', false)
const run_test = async () => {
if (!test_data.value.url) {
toast.error('The URL is required for testing.', { force: true })
return
}
try {
new URL(test_data.value.url)
} catch (e) {
toast.error('The URL is invalid.', { force: true })
return
}
test_data.value.in_progress = true
test_data.value.data.status = null
try {
const response = await request('/api/conditions/test', {
method: 'POST',
body: JSON.stringify({
url: test_data.value.url,
condition: form.filter,
}),
})
const json = await response.json()
if (!response.ok) {
toast.error(json.message || json.error || 'Unknown error', { force: true })
return
}
test_data.value.data = json
} catch (e) {
toast.error(`Failed to test condition. ${error.message}`)
} finally {
test_data.value.in_progress = false
}
}
const checkInfo = async () => {
const required = ['name', 'filter', 'cli'];
const required = ['name', 'filter', 'cli']
for (const key of required) {
if (!form[key]) {
toast.error(`The ${key} field is required.`);
toast.error(`The ${key} field is required.`)
return
}
}
if (form?.cli && '' !== form.cli) {
const options = await convertOptions(form.cli);
const options = await convertOptions(form.cli)
if (null === options) {
return
}
form.cli = form.cli.trim()
}
let copy = JSON.parse(JSON.stringify(form));
let copy = JSON.parse(JSON.stringify(form))
for (const key in copy) {
if (typeof copy[key] !== 'string') {
@ -182,7 +301,7 @@ const checkInfo = async () => {
copy[key] = copy[key].trim()
}
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) });
emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) })
}
const convertOptions = async args => {
@ -192,7 +311,7 @@ const convertOptions = async args => {
} catch (e) {
toast.error(e.message)
}
return null;
return null
}
const importItem = async () => {
@ -243,4 +362,12 @@ const importItem = async () => {
toast.error(`Failed to parse import string. ${e.message}`)
}
}
const show_data = () => {
if (!test_data.value.data?.data || !Object.keys(test_data.value.data?.data).length) {
return 'No data to show.'
}
return JSON.stringify(test_data.value.data.data, null, 2)
}
</script>

35
ui/components/Modal.vue Normal file
View file

@ -0,0 +1,35 @@
<template>
<div>
<div class="modal is-active">
<div class="model-title" v-if="title" />
<div class="modal-background" @click="closeModal"></div>
<div class="modal-content" style="width:70vw;">
<slot />
</div>
<button class="modal-close is-large" aria-label="close" @click="closeModal"></button>
</div>
</div>
</template>
<script setup>
const emitter = defineEmits(['close'])
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
})
const closeModal = () => emitter('close')
const eventFunc = e => {
if (e.key === 'Escape') {
emitter('close')
}
}
onMounted(() => window.addEventListener('keydown', eventFunc))
onUnmounted(() => window.removeEventListener('keydown', eventFunc))
</script>

View file

@ -3,14 +3,16 @@ import { useToast } from "vue-toastification"
type notificationType = 'info' | 'success' | 'warning' | 'error'
type notificationOptions = {
timeout?: number
timeout?: number,
force?: boolean,
}
const allowToast = useStorage<boolean>('allow_toasts', true)
const toast = useToast()
function notify(type: notificationType, message: string, opts?: notificationOptions): void {
if (!allowToast.value) {
let force = opts?.force || false;
if (false === allowToast.value && false === force) {
return;
}

View file

@ -82,13 +82,6 @@
<span>Delete</span>
</button>
</div>
<div class="card-footer-item">
<button class="button is-purple is-fullwidth" @click="TestCondition(cond)"
:class="{ 'is-loading': cond?.in_progress }">
<span class="icon"><i class="fa-solid fa-play" /></span>
<span>Test</span>
</button>
</div>
</div>
</div>
</div>
@ -267,8 +260,6 @@ const updateItem = async ({ reference, item }) => {
resetForm(true)
}
const filterItem = i => JSON.stringify(cleanObject(i, remove_keys), null, 2)
const editItem = _item => {
item.value = _item
itemRef.value = _item.id
@ -297,33 +288,4 @@ const exportItem = item => {
return copyText(base64UrlEncode(JSON.stringify(userData)))
}
const TestCondition = async (item) => {
const url = box.prompt("Enter URL to test condition against:")
if (!url) {
return
}
item.in_progress = true
try {
const response = await request("/api/conditions/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: url, condition: item.id || item.name }),
})
const data = await response.json()
if (!response.ok) {
toast.error(`Failed to test condition. ${data.error}`)
return
}
toast.success(data?.message || "Condition tested successfully.")
} catch (e) {
toast.error(`Failed to test condition. ${e.message}`)
} finally {
item.in_progress = false
}
}
</script>