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
|
||||
|
||||
"""
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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
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 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue