diff --git a/app/library/conditions.py b/app/library/conditions.py index 76eed61d..100b9a14 100644 --- a/app/library/conditions.py +++ b/app/library/conditions.py @@ -30,6 +30,9 @@ class Condition: cli: str """If matched append this to the download request and retry.""" + extras: dict[str, Any] = field(default_factory=dict) + """Any extra data to store with the condition.""" + def serialize(self) -> dict: return self.__dict__ @@ -137,6 +140,10 @@ class Conditions(metaclass=Singleton): item["id"] = str(uuid.uuid4()) need_save = True + if "extras" not in item: + item["extras"] = {} + need_save = True + item: Condition = init_class(Condition, item) self._items.append(item) @@ -145,7 +152,7 @@ class Conditions(metaclass=Singleton): continue if need_save: - LOG.info("Saving conditions due to format, or id change.") + LOG.info("Saving conditions due changes.") self.save(self._items) return self @@ -213,6 +220,10 @@ class Conditions(metaclass=Singleton): msg = f"Invalid command options for yt-dlp. '{e!s}'." raise ValueError(msg) from e + if not isinstance(item.get("extras"), dict): + msg = "Extras must be a dictionary." + raise ValueError(msg) # noqa: TRY004 + return True def save(self, items: list[Condition | dict]) -> "Conditions": diff --git a/ui/app/components/ConditionForm.vue b/ui/app/components/ConditionForm.vue index b29bc385..4059c84e 100644 --- a/ui/app/components/ConditionForm.vue +++ b/ui/app/components/ConditionForm.vue @@ -107,6 +107,60 @@ + +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + For advanced users only. This feature is meant to be expanded later. the only supported key right + now. ignore_download with value of true. Keys must be lowercase with + underscores (e.g., custom_field). + +
+
@@ -231,6 +285,8 @@ const config = useConfigStore() const form = reactive(JSON.parse(JSON.stringify(props.item))) const import_string = ref('') +const newExtraKey = ref('') +const newExtraValue = ref('') const test_data = ref<{ show: boolean, url: string, @@ -241,6 +297,11 @@ const test_data = ref<{ const showOptions = ref(false) const ytDlpOpt = ref([]) +// Initialize extras if not present +if (!form.extras) { + form.extras = {} +} + watch(() => config.ytdlp_options, newOptions => ytDlpOpt.value = newOptions .filter(opt => !opt.ignored) .flatMap(opt => opt.flags @@ -272,10 +333,10 @@ const checkInfo = async (): Promise => { const copy: ConditionItem = JSON.parse(JSON.stringify(form)) for (const key in copy) { - if (typeof copy[key] !== 'string') { + if (typeof (copy as any)[key] !== 'string') { continue } - copy[key] = copy[key].trim() + (copy as any)[key] = (copy as any)[key].trim() } emitter('submit', { reference: toRaw(props.reference), item: toRaw(copy) }) @@ -343,7 +404,7 @@ const importItem = async (): Promise => { return } - if ((form.filter || form.cli) && !(await box.confirm('Overwrite the current form fields?', true))) { + if ((form.filter || form.cli || Object.keys(form.extras).length > 0) && !(await box.confirm('Overwrite the current form fields?', true))) { return } @@ -359,6 +420,10 @@ const importItem = async (): Promise => { form.cli = item.cli } + if (item.extras) { + form.extras = { ...item.extras } + } + import_string.value = '' showImport.value = false } catch (e: any) { @@ -391,4 +456,95 @@ const logic_test = computed(() => { return false } }) + +// Extras management functions +const validateKey = (key: string): boolean => { + // Key must be lowercase with underscores only + return /^[a-z][a-z0-9_]*$/.test(key) +} + +const parseValue = (value: string): string | number | boolean => { + // Try to parse as number + if (!isNaN(Number(value)) && !isNaN(parseFloat(value))) { + return Number(value) + } + + // Try to parse as boolean + if (value.toLowerCase() === 'true') { + return true + } + if (value.toLowerCase() === 'false') { + return false + } + + // Return as string + return value +} + +const addExtra = (): void => { + const key = newExtraKey.value.trim() + const value = newExtraValue.value.trim() + + if (!key || !value) { + toast.error('Both key and value are required.') + return + } + + if (!validateKey(key)) { + toast.error('Key must be lowercase and contain only letters, numbers, and underscores (e.g., custom_field).') + return + } + + if (form.extras[key]) { + toast.error(`Key '${key}' already exists.`) + return + } + + form.extras[key] = parseValue(value) + newExtraKey.value = '' + newExtraValue.value = '' +} + +const removeExtra = (key: string): void => { + delete form.extras[key] +} + +const updateExtraKey = (event: Event, oldKey: string): void => { + const target = event.target as HTMLInputElement + const newKey = target.value.trim() + + if (!newKey) { + return + } + + if (!validateKey(newKey)) { + toast.error('Key must be lowercase and contain only letters, numbers, and underscores.') + target.value = oldKey // Reset to old value + return + } + + if (newKey !== oldKey) { + if (form.extras[newKey]) { + toast.error(`Key '${newKey}' already exists.`) + target.value = oldKey // Reset to old value + return + } + + // Move the value to the new key + const value = form.extras[oldKey] + delete form.extras[oldKey] + form.extras[newKey] = value + } +} + +const updateExtraValue = (key: string, event: Event): void => { + const target = event.target as HTMLInputElement + const value = target.value.trim() + + if (value) { + form.extras[key] = parseValue(value) + } else { + form.extras[key] = '' + } +} diff --git a/ui/app/pages/conditions.vue b/ui/app/pages/conditions.vue index 9dca9c90..1e1af7dc 100644 --- a/ui/app/pages/conditions.vue +++ b/ui/app/pages/conditions.vue @@ -119,13 +119,15 @@