Improved the testing logic for conditions filter.
This commit is contained in:
parent
55b2e3e7f2
commit
38bf6af1fc
5 changed files with 214 additions and 79 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
35
ui/components/Modal.vue
Normal 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>
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue