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 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( return web.json_response(
{"error": "Invalid request body expecting dict."}, {"error": "Invalid request body expecting dict."},
status=web.HTTPBadRequest.status_code, status=web.HTTPBadRequest.status_code,
) )
url = data.get("url") url = params.get("url")
if not url: if not url:
return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code) return web.json_response({"error": "url is required."}, status=web.HTTPBadRequest.status_code)
cond = data.get("condition") cond = params.get("condition")
if not cond: 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: try:
opts = {} preset = params.get("preset", self.config.default_preset)
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()
key = self.cache.hash(url + str(preset)) key = self.cache.hash(url + str(preset))
if not self.cache.has(key): 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( data = extract_info(
config=ytdlp_opts, config=ytdlp_opts,
url=url, url=url,
@ -917,6 +916,11 @@ class HttpAPI(Common):
follow_redirect=True, follow_redirect=True,
sanitize_info=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) self.cache.set(key=key, value=data, ttl=600)
else: else:
data = self.cache.get(key) data = self.cache.get(key)
@ -927,16 +931,21 @@ class HttpAPI(Common):
status=web.HTTPInternalServerError.status_code, status=web.HTTPInternalServerError.status_code,
) )
matched = Conditions.get_instance().single_match(name=cond, info=data) try:
if matched is None: from yt_dlp.utils import match_str
status = match_str(cond, data)
except Exception as e:
LOG.exception(e)
return web.json_response( return web.json_response(
data={"error": f"Condition '{cond}' doesn't match the data from the URL."}, data={"error": str(e)},
status=web.HTTPNotFound.status_code, status=web.HTTPBadRequest.status_code,
) )
return web.json_response( 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, status=web.HTTPOk.status_code,
dumps=self.encoder.encode,
) )
@route("GET", "api/presets") @route("GET", "api/presets")

View file

@ -70,7 +70,7 @@
<div class="field"> <div class="field">
<label class="label is-inline" for="filter"> <label class="label is-inline" for="filter">
<span class="icon"><i class="fa-solid fa-filter" /></span> <span class="icon"><i class="fa-solid fa-filter" /></span>
Filter Condition Filter
</label> </label>
<div class="control"> <div class="control">
<input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress" <input type="text" class="input" id="filter" v-model="form.filter" :disabled="addInProgress"
@ -78,7 +78,12 @@
</div> </div>
<span class="help"> <span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span> <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> </span>
</div> </div>
</div> </div>
@ -108,29 +113,102 @@
<div class="card-footer mt-auto"> <div class="card-footer mt-auto">
<div class="card-footer-item"> <div class="card-footer-item">
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit" <div class="card-footer-item">
:class="{ 'is-loading': addInProgress }" form="addForm"> <button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
<span class="icon"><i class="fa-solid fa-save" /></span> :class="{ 'is-loading': addInProgress }" form="addForm">
<span>Save</span> <span class="icon"><i class="fa-solid fa-save" /></span>
</button> <span>Save</span>
</div> </button>
<div class="card-footer-item"> </div>
<button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress" <div class="card-footer-item">
type="button"> <button class="button is-fullwidth is-danger" @click="emitter('cancel')" :disabled="addInProgress"
<span class="icon"><i class="fa-solid fa-times" /></span> type="button">
<span>Cancel</span> <span class="icon"><i class="fa-solid fa-times" /></span>
</button> <span>Cancel</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
</div> </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> </main>
</template> </template>
<script setup> <script setup>
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
const emitter = defineEmits(['cancel', 'submit']); const emitter = defineEmits(['cancel', 'submit'])
const props = defineProps({ const props = defineProps({
reference: { 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 toast = useNotification()
const box = useConfirm() const box = useConfirm()
const form = reactive(JSON.parse(JSON.stringify(props.item))) const form = reactive(JSON.parse(JSON.stringify(props.item)))
const import_string = ref('') 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 checkInfo = async () => {
const required = ['name', 'filter', 'cli']; const required = ['name', 'filter', 'cli']
for (const key of required) { for (const key of required) {
if (!form[key]) { if (!form[key]) {
toast.error(`The ${key} field is required.`); toast.error(`The ${key} field is required.`)
return return
} }
} }
if (form?.cli && '' !== form.cli) { if (form?.cli && '' !== form.cli) {
const options = await convertOptions(form.cli); const options = await convertOptions(form.cli)
if (null === options) { if (null === options) {
return return
} }
form.cli = form.cli.trim() form.cli = form.cli.trim()
} }
let copy = JSON.parse(JSON.stringify(form)); let copy = JSON.parse(JSON.stringify(form))
for (const key in copy) { for (const key in copy) {
if (typeof copy[key] !== 'string') { if (typeof copy[key] !== 'string') {
@ -182,7 +301,7 @@ const checkInfo = async () => {
copy[key] = copy[key].trim() 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 => { const convertOptions = async args => {
@ -192,7 +311,7 @@ const convertOptions = async args => {
} catch (e) { } catch (e) {
toast.error(e.message) toast.error(e.message)
} }
return null; return null
} }
const importItem = async () => { const importItem = async () => {
@ -243,4 +362,12 @@ const importItem = async () => {
toast.error(`Failed to parse import string. ${e.message}`) 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> </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 notificationType = 'info' | 'success' | 'warning' | 'error'
type notificationOptions = { type notificationOptions = {
timeout?: number timeout?: number,
force?: boolean,
} }
const allowToast = useStorage<boolean>('allow_toasts', true) const allowToast = useStorage<boolean>('allow_toasts', true)
const toast = useToast() const toast = useToast()
function notify(type: notificationType, message: string, opts?: notificationOptions): void { function notify(type: notificationType, message: string, opts?: notificationOptions): void {
if (!allowToast.value) { let force = opts?.force || false;
if (false === allowToast.value && false === force) {
return; return;
} }

View file

@ -82,13 +82,6 @@
<span>Delete</span> <span>Delete</span>
</button> </button>
</div> </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> </div>
</div> </div>
@ -267,8 +260,6 @@ const updateItem = async ({ reference, item }) => {
resetForm(true) resetForm(true)
} }
const filterItem = i => JSON.stringify(cleanObject(i, remove_keys), null, 2)
const editItem = _item => { const editItem = _item => {
item.value = _item item.value = _item
itemRef.value = _item.id itemRef.value = _item.id
@ -297,33 +288,4 @@ const exportItem = item => {
return copyText(base64UrlEncode(JSON.stringify(userData))) 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> </script>