From 5af46554297e66cfcd9dd31d3b602eceb9b82438 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 16:40:51 +0300 Subject: [PATCH 01/13] Fix not being able to add --sponsorblock-mark --- app/library/Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/library/Utils.py b/app/library/Utils.py index cc5f5018..dbb6c1f7 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -453,7 +453,7 @@ def arg_converter( if isinstance(matchFilter, set): diff["match_filter"] = {"filters": list(matchFilter)} - return json.loads(json.dumps(diff, cls=Encoder)) + return json.loads(json.dumps(diff, cls=Encoder, default=str)) return diff From 0306f9e61908f80751148017a20fda4982c819fd Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 17:01:38 +0300 Subject: [PATCH 02/13] Add try catch block for url decoding. Fix #287 --- ui/utils/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/utils/index.js b/ui/utils/index.js index d7fefd9d..2998280a 100644 --- a/ui/utils/index.js +++ b/ui/utils/index.js @@ -221,7 +221,12 @@ const encodePath = item => { } item = item.replace(/#/g, '%23') - return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/') + try { + return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/') + } catch (e) { + console.error('Error encoding path:', e, item) + return item + } } /** From c13c4427a7b772ff59e261649d7965554b19b83c Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 17:44:23 +0300 Subject: [PATCH 03/13] Proxy our toast usage via our own methods to be able to control enabling/disabling it. --- ui/assets/css/style.css | 5 +++- ui/components/ConditionForm.vue | 2 +- ui/components/FloatingImage.vue | 2 +- ui/components/GetInfo.vue | 2 +- ui/components/History.vue | 2 +- ui/components/ImageView.vue | 2 +- ui/components/NewDownload.vue | 2 +- ui/components/NotificationForm.vue | 2 +- ui/components/PresetForm.vue | 2 +- ui/components/TaskForm.vue | 2 +- ui/components/VideoPlayer.vue | 2 +- ui/composables/useNotification.ts | 47 ++++++++++++++++++++++++++++++ ui/composables/useToast.js | 2 -- ui/pages/browser/[...slug].vue | 2 +- ui/pages/conditions.vue | 2 +- ui/pages/logs.vue | 2 +- ui/pages/notifications.vue | 2 +- ui/pages/presets.vue | 2 +- ui/pages/tasks.vue | 2 +- ui/stores/SocketStore.js | 2 +- ui/utils/index.js | 4 +-- 21 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 ui/composables/useNotification.ts delete mode 100644 ui/composables/useToast.js diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css index 1894c280..fb8781d3 100644 --- a/ui/assets/css/style.css +++ b/ui/assets/css/style.css @@ -325,7 +325,10 @@ hr { white-space: nowrap; } - .is-max-width { max-width: calc(100% - 10px); } + +.is-bold { + font-weight: bold; +} diff --git a/ui/components/ConditionForm.vue b/ui/components/ConditionForm.vue index 144940c6..665b3d8c 100644 --- a/ui/components/ConditionForm.vue +++ b/ui/components/ConditionForm.vue @@ -149,7 +149,7 @@ const props = defineProps({ }, }) -const toast = useToast() +const toast = useNotification() const form = reactive(JSON.parse(JSON.stringify(props.item))) const import_string = ref('') const showImport = useStorage('showImport', false); diff --git a/ui/components/FloatingImage.vue b/ui/components/FloatingImage.vue index 1ac1e289..2c346b57 100644 --- a/ui/components/FloatingImage.vue +++ b/ui/components/FloatingImage.vue @@ -43,7 +43,7 @@ const props = defineProps({ }); const cache = useSessionCache() -const toast = useToast() +const toast = useNotification() const url = ref() const error = ref(false) const isPreloading = ref(false) diff --git a/ui/components/GetInfo.vue b/ui/components/GetInfo.vue index 273312a9..5135dd2c 100644 --- a/ui/components/GetInfo.vue +++ b/ui/components/GetInfo.vue @@ -40,7 +40,7 @@ import { request } from '~/utils/index' const emitter = defineEmits(['closeModel']) const isLoading = ref(false) const data = ref({}) -const toast = useToast() +const toast = useNotification() const props = defineProps({ link: { diff --git a/ui/components/History.vue b/ui/components/History.vue index a5954de4..d77b90da 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -385,7 +385,7 @@ const emitter = defineEmits(['getInfo', 'add_new']) const config = useConfigStore() const stateStore = useStateStore() const socket = useSocketStore() -const toast = useToast() +const toast = useNotification() const selectedElms = ref([]) const masterSelectAll = ref(false) diff --git a/ui/components/ImageView.vue b/ui/components/ImageView.vue index 669316d0..8a5a22a4 100644 --- a/ui/components/ImageView.vue +++ b/ui/components/ImageView.vue @@ -23,7 +23,7 @@ import { request } from '~/utils/index' const emitter = defineEmits(['closeModel']) const isLoading = ref(false) const data = ref({}) -const toast = useToast() +const toast = useNotification() const image = ref('') const props = defineProps({ diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index 172a0d48..61a415a2 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -189,7 +189,7 @@ const props = defineProps({ const emitter = defineEmits(['getInfo', 'clear_form']) const config = useConfigStore() const socket = useSocketStore() -const toast = useToast() +const toast = useNotification() const showAdvanced = useStorage('show_advanced', false) const addInProgress = ref(false) diff --git a/ui/components/NotificationForm.vue b/ui/components/NotificationForm.vue index 87130c3f..90a9dcff 100644 --- a/ui/components/NotificationForm.vue +++ b/ui/components/NotificationForm.vue @@ -268,7 +268,7 @@ diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index d544a7fa..a6d1e183 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -232,11 +232,12 @@ import { useStorage } from '@vueuse/core' import { CronExpressionParser } from 'cron-parser' -const emitter = defineEmits(['cancel', 'submit']); -const toast = useNotification(); -const config = useConfigStore(); -const showImport = useStorage('showImport', false); -const import_string = ref(''); +const emitter = defineEmits(['cancel', 'submit']) +const toast = useNotification() +const config = useConfigStore() +const showImport = useStorage('showImport', false) +const import_string = ref('') +const box = useConfirm() const props = defineProps({ reference: { @@ -255,49 +256,49 @@ const props = defineProps({ }, }) -const form = reactive(props.task); +const form = reactive(props.task) onMounted(() => { if (!props.task?.preset || '' === props.task.preset) { - form.preset = toRaw(config.app.default_preset); + form.preset = toRaw(config.app.default_preset) } }) const checkInfo = async () => { - const required = ['name', 'url']; + const required = ['name', 'url'] for (const key of required) { if (!form[key]) { - toast.error(`The ${key} field is required.`); - return; + toast.error(`The ${key} field is required.`) + return } } if (form.timer) { try { - CronExpressionParser.parse(form.timer); + CronExpressionParser.parse(form.timer) } catch (e) { console.error(e) - toast.error(`Invalid CRON expression. ${e.message}`); - return; + toast.error(`Invalid CRON expression. ${e.message}`) + return } } try { - new URL(form.url); + new URL(form.url) } catch (e) { - toast.error('Invalid URL'); - return; + toast.error('Invalid URL') + 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(" ") } - emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) }); + emitter('submit', { reference: toRaw(props.reference), task: toRaw(form) }) } const importItem = async () => { @@ -327,7 +328,7 @@ const importItem = async () => { } if (form.url || form.timer) { - if (false === confirm('This will overwrite the current form fields. Are you sure?')) { + if (false === box.confirm('This will overwrite the current form fields. Are you sure?', true)) { return } } @@ -391,7 +392,7 @@ const convertOptions = async args => { toast.error(e.message) } - return null; + return null } const hasFormatInConfig = computed(() => { diff --git a/ui/composables/useConfirm.ts b/ui/composables/useConfirm.ts new file mode 100644 index 00000000..45976a4d --- /dev/null +++ b/ui/composables/useConfirm.ts @@ -0,0 +1,19 @@ +import { useStorage } from '@vueuse/core' + +const reduceConfirm = useStorage('reduce_confirm', false) + +function confirm(msg: string, force: boolean = false): boolean { + if (false === force && true === reduceConfirm.value) { + return true + } + + return window.confirm(msg) +} + +function alert(msg: string): boolean { + return window.confirm(msg) +} + +export default function useConfirm() { + return { confirm, alert, reduceConfirm } +} diff --git a/ui/pages/conditions.vue b/ui/pages/conditions.vue index 075efb03..96433fa9 100644 --- a/ui/pages/conditions.vue +++ b/ui/pages/conditions.vue @@ -116,6 +116,7 @@ import { request } from '~/utils/index' const toast = useNotification() const config = useConfigStore() const socket = useSocketStore() +const box = useConfirm() const items = ref([]) const item = ref({}) @@ -213,7 +214,7 @@ const updateItems = async items => { } const deleteItem = async (item) => { - if (true !== confirm(`Delete '${item.name}'?`)) { + if (true !== box.confirm(`Delete '${item.name}'?`, true)) { return } diff --git a/ui/pages/index.vue b/ui/pages/index.vue index 2486aaff..b0e1f7ae 100644 --- a/ui/pages/index.vue +++ b/ui/pages/index.vue @@ -62,6 +62,8 @@ const emitter = defineEmits(['getInfo']) const config = useConfigStore() const stateStore = useStateStore() const socket = useSocketStore() +const box = useConfirm() + const get_info = ref('') const bg_enable = useStorage('random_bg', true) const bg_opacity = useStorage('random_bg_opacity', 0.85) @@ -91,7 +93,7 @@ watch(() => stateStore.queue, () => { }, { deep: true }) const pauseDownload = () => { - if (false === confirm('Are you sure you want to pause all non-active downloads?')) { + if (false === box.confirm('Are you sure you want to pause all non-active downloads?')) { return false } diff --git a/ui/pages/notifications.vue b/ui/pages/notifications.vue index dc57249f..9a3457a4 100644 --- a/ui/pages/notifications.vue +++ b/ui/pages/notifications.vue @@ -143,6 +143,7 @@ import { request } from '~/utils/index' const toast = useNotification() const config = useConfigStore() const socket = useSocketStore() +const box = useConfirm() const allowedEvents = ref([]) const notifications = ref([]) @@ -234,7 +235,7 @@ const updateData = async notifications => { } const deleteItem = async item => { - if (true !== confirm(`Are you sure you want to delete notification target (${item.name})?`)) { + if (true !== box.confirm(`Are you sure you want to delete notification target (${item.name})?`)) { return } @@ -294,7 +295,7 @@ const editItem = item => { const join_events = events => (!events || events.length < 1) ? 'ALL' : events.map(e => ucFirst(e)).join(', ') const sendTest = async () => { - if (true !== confirm('Are you sure you want to send a test notification?')) { + if (true !== box.confirm('Are you sure you want to send a test notification?')) { return } diff --git a/ui/pages/presets.vue b/ui/pages/presets.vue index 90864b17..fc9a80b3 100644 --- a/ui/pages/presets.vue +++ b/ui/pages/presets.vue @@ -152,6 +152,7 @@ import { request } from '~/utils/index' const toast = useNotification() const config = useConfigStore() const socket = useSocketStore() +const box = useConfirm() const presets = ref([]) const preset = ref({}) @@ -249,7 +250,7 @@ const updatePresets = async (items) => { } const deleteItem = async (item) => { - if (true !== confirm(`Delete preset '${item.name}'?`)) { + if (true !== box.confirm(`Delete preset '${item.name}'?`, true)) { return } diff --git a/ui/pages/tasks.vue b/ui/pages/tasks.vue index c8e19e8b..1d3171b5 100644 --- a/ui/pages/tasks.vue +++ b/ui/pages/tasks.vue @@ -125,6 +125,7 @@ import { request } from '~/utils/index' const toast = useNotification() const config = useConfigStore() const socket = useSocketStore() +const box = useConfirm() const tasks = ref([]) const task = ref({}) @@ -216,7 +217,7 @@ const updateTasks = async items => { } const deleteItem = async item => { - if (true !== confirm(`Are you sure you want to delete (${item.name})?`)) { + if (true !== box.confirm(`Are you sure you want to delete (${item.name})?`, true)) { return } @@ -290,7 +291,7 @@ const tryParse = expression => { } const runNow = item => { - if (true !== confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) { + if (true !== box.confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) { return } From 23c74d8447a45d1eead81e4c3e7e6f0d1a92bc7f Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 18:32:13 +0300 Subject: [PATCH 05/13] improve task page information display. --- ui/components/TaskForm.vue | 34 +++++++++++++++++++++++++--------- ui/pages/tasks.vue | 18 ++++++++++++++---- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index a6d1e183..7f93eac1 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -111,8 +111,9 @@ Select the preset to use for this URL. If the - -f, --format argument is present in the command line options, the preset and all - it's options will be ignored. + -f, --format + argument is present in the command line options, the preset and all + it's options will be ignored. @@ -152,8 +153,8 @@ - Paths are relative to global download path, defaults to preset download path if set otherwise, - fallback root path if not set. + Current download folder: {{ get_download_folder() }}. All folders are + sub-folders of {{ config.app.download_path }}. @@ -170,11 +171,7 @@ - Use this output template if non are given with URL. if not set, it will defaults to - {{ config.app.output_template }}. - For more information visit this url. - + Current output format: {{ get_output_template() }}. @@ -405,4 +402,23 @@ const hasFormatInConfig = computed(() => { const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag) +const get_download_folder = () => { + if (form.preset && !hasFormatInConfig.value) { + const preset = config.presets.find(p => p.name === form.preset) + if (preset && preset.folder) { + return preset.folder.replace(config.app.download_path, '') + } + } + return '/' +} + +const get_output_template = () => { + if (form.preset && !hasFormatInConfig.value) { + const preset = config.presets.find(p => p.name === form.preset) + if (preset && preset.template) { + return preset.template + } + } + return config.app.output_template || '%(title)s.%(ext)s' +} diff --git a/ui/pages/tasks.vue b/ui/pages/tasks.vue index 1d3171b5..849f3c0f 100644 --- a/ui/pages/tasks.vue +++ b/ui/pages/tasks.vue @@ -101,8 +101,9 @@ div.is-centered { @@ -134,6 +135,7 @@ const toggleForm = ref(false) const isLoading = ref(false) const initialLoad = ref(true) const addInProgress = ref(false) +const runNowInProgress = ref(false) watch(() => config.app.basic_mode, async () => { if (!config.app.basic_mode) { @@ -217,7 +219,7 @@ const updateTasks = async items => { } const deleteItem = async item => { - if (true !== box.confirm(`Are you sure you want to delete (${item.name})?`, true)) { + if (true !== box.confirm(`Delete '${item.name}' task?`, true)) { return } @@ -290,11 +292,13 @@ const tryParse = expression => { } } -const runNow = item => { +const runNow = async item => { if (true !== box.confirm(`Run '${item.name}' now? it will also run at the scheduled time.`)) { return } + runNowInProgress.value = true + let data = { url: item.url, preset: item.preset, @@ -313,6 +317,11 @@ const runNow = item => { } socket.emit('add_url', data) + + setTimeout(async () => { + await nextTick() + runNowInProgress.value = false + }, 500) } onUnmounted(() => socket.off('status', statusHandler)) @@ -350,4 +359,5 @@ const exportItem = async item => { return copyText(base64UrlEncode(JSON.stringify(data))); } + From bdf50d0ed86f9027dd799d55829dfe71001bb5b3 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 19:11:55 +0300 Subject: [PATCH 06/13] Make it possible to create tasks without timer. --- app/library/HttpAPI.py | 3 --- app/library/Tasks.py | 22 ++++++++++++++++------ ui/components/TaskForm.vue | 11 +++++------ ui/pages/tasks.vue | 12 +++++++----- ui/utils/index.js | 17 +++++++++++++++++ 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index f922e84a..b97c8dd1 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -1007,9 +1007,6 @@ class HttpAPI(Common): if not item.get("id", None) or not validate_uuid(item.get("id"), version=4): item["id"] = str(uuid.uuid4()) - if not item.get("timer", None) or str(item.get("timer")).strip() == "": - item["timer"] = f"{random.randint(1,59)} */1 * * *" # noqa: S311 - if not item.get("template", None): item["template"] = "" diff --git a/app/library/Tasks.py b/app/library/Tasks.py index ed3a448e..e529a54f 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -108,7 +108,7 @@ class Tasks(metaclass=Singleton): self.load() self._notify.subscribe( Events.TASKS_ADD, - lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005 + lambda data, _, **kwargs: self.save(**data.data), # noqa: ARG005 f"{__class__.__name__}.add", ) @@ -145,6 +145,7 @@ class Tasks(metaclass=Singleton): for i, task in enumerate(tasks): try: task, task_status = clean_item(task, keys=("cookies", "config")) + self.validate(task) task = Task(**task) if task_status: need_save = True @@ -152,9 +153,13 @@ class Tasks(metaclass=Singleton): LOG.error(f"Failed to parse task at list position '{i}'. '{e!s}'.") continue + self._tasks.append(task) + + if not task.timer: + continue + try: self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=task.id) - self._tasks.append(task) try: from cronsim import CronSim @@ -221,14 +226,19 @@ class Tasks(metaclass=Singleton): msg = "No name found." raise ValueError(msg) - if not task.get("timer"): - msg = "No timer found." - raise ValueError(msg) - if not task.get("url"): msg = "No URL found." raise ValueError(msg) + if task.get("timer"): + try: + from cronsim import CronSim + + CronSim(task.get("timer"), datetime.now(UTC)) + except Exception as e: + msg = f"Invalid timer format. '{e!s}'." + raise ValueError(msg) from e + if task.get("cli"): try: from .Utils import arg_converter diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index 7f93eac1..fcf0b1ef 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -126,16 +126,15 @@ CRON expression timer.
- +
- The CRON timer expression to use for this task. If not set, the task will run once an hour in a - random - minute. For more information on CRON expressions, see crontab.guru. + The CRON timer expression to use for this task. If not set, the task will be disabled. For more + information on CRON expressions, see + crontab.guru. diff --git a/ui/pages/tasks.vue b/ui/pages/tasks.vue index 849f3c0f..f56c486f 100644 --- a/ui/pages/tasks.vue +++ b/ui/pages/tasks.vue @@ -67,7 +67,8 @@ div.is-centered {

- {{ tryParse(item.timer) }} - {{ item.timer }} + {{ tryParse(item.timer) }} - {{ item.timer }} + No timer is set

@@ -102,7 +103,7 @@ div.is-centered {

-
{{ filterItem(cond) }}
+
{{ JSON.stringify(cleanObject(cond, remove_keys), null, 2) }}
+ @@ -104,6 +111,10 @@ filter i am able to bypass it availability = 'needs_auth' & channel_id = 'channel_id'. and set proxy for that specific video, while leaving the rest of the videos to be downloaded normally. +
  • + The data which the filter is applied on is the same data that yt-dlp returns, simply, click on the + information button, and check the data to craft your filter. +
  • @@ -125,6 +136,7 @@ const toggleForm = ref(false) const isLoading = ref(false) const initialLoad = ref(true) const addInProgress = ref(false) +const remove_keys = ['in_progress', 'raw'] watch(() => config.app.basic_mode, async () => { if (!config.app.basic_mode) { @@ -236,6 +248,7 @@ const deleteItem = async (item) => { } const updateItem = async ({ reference, item }) => { + item = cleanObject(item, remove_keys) if (reference) { const index = items.value.findIndex(t => t?.id === reference) if (index > -1) { @@ -254,10 +267,7 @@ const updateItem = async ({ reference, item }) => { resetForm(true) } -const filterItem = item => { - const { raw, ...rest } = item - return JSON.stringify(rest, null, 2) -} +const filterItem = i => JSON.stringify(cleanObject(i, remove_keys), null, 2) const editItem = _item => { item.value = _item @@ -287,4 +297,33 @@ 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 + } +} From 38bf6af1fc526b9b783d6c481fc6f39dbba196a9 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 22:15:06 +0300 Subject: [PATCH 08/13] Improved the testing logic for conditions filter. --- app/library/HttpAPI.py | 43 +++++--- ui/components/ConditionForm.vue | 171 ++++++++++++++++++++++++++---- ui/components/Modal.vue | 35 ++++++ ui/composables/useNotification.ts | 6 +- ui/pages/conditions.vue | 38 ------- 5 files changed, 214 insertions(+), 79 deletions(-) create mode 100644 ui/components/Modal.vue diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index 7d050ac3..b323f713 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -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") diff --git a/ui/components/ConditionForm.vue b/ui/components/ConditionForm.vue index ea09e06c..4e7b4f7c 100644 --- a/ui/components/ConditionForm.vue +++ b/ui/components/ConditionForm.vue @@ -70,7 +70,7 @@
    - The yt-dlp [--match-filters] filter logic. + + The yt-dlp [--match-filters] filter logic. + + Test filter logic + +
    @@ -108,29 +113,102 @@ + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + + + The url to test the filter against. + +
    + +
    + +
    + +
    + + + The yt-dlp [--match-filters] filter logic.
    +
    +
    + +
    + + + Filter Status: {{ test_data?.data?.status === null ? 'Not tested' : test_data?.data?.status ? + 'Matched' : 'Not matched' }} + +
    + +
    +
    {{ show_data() }}
    +
    + +
    +
    + +
    +
    diff --git a/ui/components/Modal.vue b/ui/components/Modal.vue new file mode 100644 index 00000000..20582c57 --- /dev/null +++ b/ui/components/Modal.vue @@ -0,0 +1,35 @@ + + + diff --git a/ui/composables/useNotification.ts b/ui/composables/useNotification.ts index f24ee24d..1e5872e9 100644 --- a/ui/composables/useNotification.ts +++ b/ui/composables/useNotification.ts @@ -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('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; } diff --git a/ui/pages/conditions.vue b/ui/pages/conditions.vue index 72694b2a..cbd080dc 100644 --- a/ui/pages/conditions.vue +++ b/ui/pages/conditions.vue @@ -82,13 +82,6 @@ Delete - @@ -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 - } -} From 1de2de9750e82384a31db8d0b16cac0b0cfe49f4 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 22:37:21 +0300 Subject: [PATCH 09/13] Give more control over the toasts. --- ui/components/Settings.vue | 38 +++++++++++++++++++++++++++---- ui/composables/useNotification.ts | 13 ++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/ui/components/Settings.vue b/ui/components/Settings.vue index cf64c3da..184196c5 100644 --- a/ui/components/Settings.vue +++ b/ui/components/Settings.vue @@ -12,7 +12,7 @@
    - +
    - +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + + +
    +
    +
    @@ -103,10 +131,9 @@
    - diff --git a/ui/composables/useNotification.ts b/ui/composables/useNotification.ts index 1e5872e9..dda3f462 100644 --- a/ui/composables/useNotification.ts +++ b/ui/composables/useNotification.ts @@ -1,13 +1,17 @@ import { useStorage } from '@vueuse/core' -import { useToast } from "vue-toastification" +import { POSITION, useToast } from "vue-toastification" type notificationType = 'info' | 'success' | 'warning' | 'error' type notificationOptions = { timeout?: number, force?: boolean, + closeOnClick?: boolean, + position?: POSITION } const allowToast = useStorage('allow_toasts', true) +const toastPosition = useStorage('toast_position', POSITION.TOP_RIGHT) +const toastDismissOnClick = useStorage('toast_dismiss_on_click', true) const toast = useToast() function notify(type: notificationType, message: string, opts?: notificationOptions): void { @@ -20,6 +24,13 @@ function notify(type: notificationType, message: string, opts?: notificationOpti opts = {} } + if (opts?.force) { + delete opts.force + } + + opts.closeOnClick = toastDismissOnClick.value + opts.position = toastPosition.value ?? POSITION.TOP_RIGHT + switch (type) { case 'info': toast.info(message, opts) From 1af17552f9e96f3bef38ad2c458b98dffc9a9c63 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 3 Jun 2025 23:46:29 +0300 Subject: [PATCH 10/13] Enhance UI layout and loading state for changelog page --- ui/layouts/default.vue | 2 +- ui/pages/changelog.vue | 105 +++++++++++++++++++++++++++++------------ 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/ui/layouts/default.vue b/ui/layouts/default.vue index 11bb176d..9319a463 100644 --- a/ui/layouts/default.vue +++ b/ui/layouts/default.vue @@ -87,7 +87,7 @@