Merge pull request #210 from arabcoders/dev
Standardize the form design & add import/export to all functions
This commit is contained in:
commit
9adb6efbc5
12 changed files with 1221 additions and 1195 deletions
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
|
|
@ -43,6 +43,19 @@
|
|||
"YTP_ACCESS_LOG": "true",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Node: Generate UI",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"generate"
|
||||
],
|
||||
"runtimeExecutable": "yarn",
|
||||
"type": "node",
|
||||
"cwd": "${workspaceFolder}/ui",
|
||||
"console": "internalConsole",
|
||||
"outputCapture": "std",
|
||||
},
|
||||
{
|
||||
"name": "Python: main.py (Deep)",
|
||||
"type": "debugpy",
|
||||
|
|
|
|||
36
Pipfile.lock
generated
36
Pipfile.lock
generated
|
|
@ -27,11 +27,11 @@
|
|||
},
|
||||
"aiohappyeyeballs": {
|
||||
"hashes": [
|
||||
"sha256:0850b580748c7071db98bffff6d4c94028d0d3035acc20fd721a0ce7e8cac35d",
|
||||
"sha256:18fde6204a76deeabc97c48bdd01d5801cfda5d6b9c8bbeb1aaaee9d648ca191"
|
||||
"sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558",
|
||||
"sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.5.0"
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
|
|
@ -149,11 +149,11 @@
|
|||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e",
|
||||
"sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"
|
||||
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
|
||||
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.1.0"
|
||||
"version": "==25.3.0"
|
||||
},
|
||||
"bidict": {
|
||||
"hashes": [
|
||||
|
|
@ -436,20 +436,20 @@
|
|||
},
|
||||
"curl-cffi": {
|
||||
"hashes": [
|
||||
"sha256:202145686c5ebc00635b9b15cc8afa82523f14197a640b618bb09d59e74889ba",
|
||||
"sha256:26d61c5b5c59e9c4a006cba35feccd73c75ce981db0da89a3da88dd280ead7a0",
|
||||
"sha256:429a14e724898b7001be2776caa87c294e8062f0ac652619df5854eeae6e156c",
|
||||
"sha256:4818e074b61cb209bd8d4d0d03783313d4773e6b51f8b815e25aad9cc146a7b7",
|
||||
"sha256:752a4686c062a8bd6e25961845ea812704ba089a0e6863957deb2a30590105bb",
|
||||
"sha256:7968c3ea37bec96bbe4b623c0fa59c09d3cdaf173da2e0a516b19db52f05caa4",
|
||||
"sha256:7b1177203e47d3248ccb250e63983eaddf39fce5332f91cc2993ea54cae5aa21",
|
||||
"sha256:7f41bd0a5dafe3fe19319da1c31302a40634ec4779769ca3d954d8673eb2b4f2",
|
||||
"sha256:81ba09622e68f8392236212dd0afd75dcd18cbe1be16467deb4d441d62cfc8a3",
|
||||
"sha256:ea8f0511bc6882d30a985ddc53fa26d109f2977715d4851aef162bfa308cd2a8"
|
||||
"sha256:04b1d23f0f54f94b8298ed417e6bece85a635d674723cde2b155da686efbf78f",
|
||||
"sha256:15053d01c6a3e3c4c5331ce9e07e1dc31ca5aa063babca05d18b1b5aad369fac",
|
||||
"sha256:1e60b8ecc80bfb0da4ff73ac9d194e80482b50ecbb8aefec1b0edaf45fafd80e",
|
||||
"sha256:3969e4260ad4dab638fb6dbe349623f9f5f022435c7fd21daf760231380367fa",
|
||||
"sha256:3e37b35268ca58492f54ed020ae4b50c33ee0debad4145db9f746f04ed466eb0",
|
||||
"sha256:458f53c41bd76d90d8974d60c3a8a0dd902a1af1f9056215cf24f454bcedc6fd",
|
||||
"sha256:59389773a1556e087120e91eac1e33f84f1599d853e1bc168b153e4cdf360002",
|
||||
"sha256:cfc74f09e44d2d8d61b8e8fda3a7004b5bc0217a703fbbe9e16ef8caa1f3d4e4",
|
||||
"sha256:f03f4b17dc679c82bd3c946feb1ad38749b2ad731d7c26daefaac857d1c72fd9",
|
||||
"sha256:f1b0c7b7b81afca15a0e56c593d3c2bdcd4fd4c9ca49b9ded5b9d8076ba78ff9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.9.0"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"debugpy": {
|
||||
"hashes": [
|
||||
|
|
|
|||
|
|
@ -360,7 +360,10 @@ class Notification(metaclass=Singleton):
|
|||
method=target.get("request", {}).get("method", "POST"),
|
||||
url=target.get("request", {}).get("url"),
|
||||
headers=[
|
||||
TargetRequestHeader(key=h.get("key"), value=h.get("value"))
|
||||
TargetRequestHeader(
|
||||
key=str(h.get("key", "")).strip(),
|
||||
value=str(h.get("value", "")).strip(),
|
||||
)
|
||||
for h in target.get("request", {}).get("headers", [])
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class YTDLPOpts(metaclass=Singleton):
|
|||
self.presets_opts = {}
|
||||
self._item_opts = {}
|
||||
|
||||
if "format" in data and "not_set" == data["format"]:
|
||||
if "format" in data and data["format"] in ["not_set", "default"]:
|
||||
data["format"] = None
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -1,202 +1,243 @@
|
|||
<template>
|
||||
<main class="columns mt-2">
|
||||
<div class="column">
|
||||
<form id="taskForm" @submit.prevent="checkInfo()">
|
||||
<div class="box">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-12">
|
||||
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
||||
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
<main class="columns mt-2 is-multiline">
|
||||
<div class="column is-12">
|
||||
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
|
||||
<div class="card">
|
||||
|
||||
<div class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
||||
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name">
|
||||
Target name
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The notification target name, this is used to identify the target in the logs and
|
||||
notifications.</span>
|
||||
<div class="card-header-icon" v-if="reference">
|
||||
<button type="button" @click="showImport = !showImport">
|
||||
<span class="icon"><i class="fa-solid" :class="{
|
||||
'fa-arrow-down': !showImport,
|
||||
'fa-arrow-up': showImport,
|
||||
}" /></span>
|
||||
<span>
|
||||
<span v-if="showImport">Hide</span>
|
||||
<span v-else>Show</span>
|
||||
import
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="url">
|
||||
Target URL
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="url" class="input" id="url" v-model="form.request.url" :disabled="addInProgress"
|
||||
required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The URL to send the notification to.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="method">
|
||||
Request method
|
||||
<div class="column is-12" v-if="showImport || !reference">
|
||||
<label class="label is-inline" for="import_string">
|
||||
Import string
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="method" class="is-fullwidth" v-model="form.request.method" :disabled="addInProgress">
|
||||
<option v-for="item, index in requestMethods" :key="`${index}-${item}`" :value="item">
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left is-expanded">
|
||||
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
The request method to use when sending the notification. This can be any of the standard HTTP
|
||||
methods.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="type">
|
||||
Request Type
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="type" class="is-fullwidth" v-model="form.request.type" :disabled="addInProgress">
|
||||
<option v-for="item, index in requestType" :key="`${index}-${item}`" :value="item">
|
||||
{{ ucFirst(item) }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="control">
|
||||
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
|
||||
<span class="icon"><i class="fa-solid fa-add" /></span>
|
||||
<span>Import</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
The request type to use when sending the notification. This can be <code>JSON</code> or
|
||||
<code>FORM</code> request.
|
||||
</span>
|
||||
<span>You can use this field to populate the data, using shared string.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12 is-clearfix">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="on">
|
||||
Select Events
|
||||
<template v-if="form.on.length > 0">
|
||||
- <NuxtLink @click="form.on = []">Clear selection</NuxtLink>
|
||||
</template>
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-multiple is-fullwidth">
|
||||
<select id="on" class="is-fullwidth" v-model="form.on" :disabled="addInProgress" multiple>
|
||||
<option v-for="item, index in allowedEvents" :key="`${index}-${item}`" :value="item">
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name">
|
||||
Target name
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
Subscribe to the events you want to listen for. When the event is triggered, the notification will
|
||||
be sent to the target URL. If no events are selected, the notification will be sent for all events.
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The notification target name, this is used to identify the target in the logs and
|
||||
notifications.</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label is-inline is-unselectable">
|
||||
Optional Headers - <button type="button" class="has-text-link"
|
||||
@click="form.request.headers.push({ key: '', value: '' });">Add Header
|
||||
</button>
|
||||
</label>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<template v-for="_, key in form.request.headers" :key="key">
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="form.request.headers[key].key"
|
||||
:disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="url">
|
||||
Target URL
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="url" class="input" id="url" v-model="form.request.url" :disabled="addInProgress"
|
||||
required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The URL to send the notification to.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="method">
|
||||
Request method
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="method" class="is-fullwidth" v-model="form.request.method" :disabled="addInProgress">
|
||||
<option v-for="item, index in requestMethods" :key="`${index}-${item}`" :value="item">
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
The request method to use when sending the notification. This can be any of the standard HTTP
|
||||
methods.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="type">
|
||||
Request Type
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="type" class="is-fullwidth" v-model="form.request.type" :disabled="addInProgress">
|
||||
<option v-for="item, index in requestType" :key="`${index}-${item}`" :value="item">
|
||||
{{ ucFirst(item) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
The request type to use when sending the notification. This can be <code>JSON</code> or
|
||||
<code>FORM</code> request.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12 is-clearfix">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="on">
|
||||
Select Events
|
||||
<template v-if="form.on.length > 0">
|
||||
- <NuxtLink @click="form.on = []">Clear selection</NuxtLink>
|
||||
</template>
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-multiple is-fullwidth">
|
||||
<select id="on" class="is-fullwidth" v-model="form.on" :disabled="addInProgress" multiple>
|
||||
<option v-for="item, index in allowedEvents" :key="`${index}-${item}`" :value="item">
|
||||
{{ item }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
Subscribe to the events you want to listen for. When the event is triggered, the notification will
|
||||
be sent to the target URL. If no events are selected, the notification will be sent for all
|
||||
events.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field">
|
||||
<label class="label is-inline is-unselectable">
|
||||
Optional Headers - <button type="button" class="has-text-link"
|
||||
@click="form.request.headers.push({ key: '', value: '' });">Add Header
|
||||
</button>
|
||||
</label>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<template v-for="_, key in form.request.headers" :key="key">
|
||||
<div class="column is-5">
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="form.request.headers[key].key"
|
||||
:disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The header key to send with the notification.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="form.request.headers[key].value"
|
||||
:disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The header value to send with the notification.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<div class="control">
|
||||
<button type="button" class="button is-danger" @click="form.request.headers.splice(key, 1)"
|
||||
:disabled="addInProgress">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The header key to send with the notification.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" v-model="form.request.headers[key].value"
|
||||
:disabled="addInProgress" required>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The header value to send with the notification.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="column is-1">
|
||||
<div class="control">
|
||||
<button type="button" class="button is-danger" @click="form.request.headers.splice(key, 1)"
|
||||
:disabled="addInProgress">
|
||||
<span class="icon"><i class="fa-solid fa-trash" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
|
||||
<span class="has-text-danger">
|
||||
If header key or value is empty, the header will not be sent.
|
||||
</template>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
|
||||
<span class="has-text-danger">
|
||||
If header key or value is empty, the header will not be sent.
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<p class="control">
|
||||
<button class="button is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="taskForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button">
|
||||
<span class="icon"><i class="fa-solid fa-times" /></span>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<p class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="taskForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</p>
|
||||
<p 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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -205,6 +246,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const toast = useToast();
|
||||
const props = defineProps({
|
||||
|
|
@ -231,6 +273,8 @@ const props = defineProps({
|
|||
const form = reactive(props.item);
|
||||
const requestMethods = ['POST', 'PUT'];
|
||||
const requestType = ['json', 'form'];
|
||||
const showImport = useStorage('showImport', false);
|
||||
const import_string = ref('');
|
||||
|
||||
const checkInfo = async () => {
|
||||
const required = ['name', 'request.url', 'request.method', 'request.type'];
|
||||
|
|
@ -254,13 +298,73 @@ const checkInfo = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
let headers = []
|
||||
|
||||
for (const header of form.request.headers) {
|
||||
if (!header.key || !header.value) {
|
||||
form.request.headers.splice(form.request.headers.indexOf(header), 1);
|
||||
continue
|
||||
}
|
||||
headers.push({ key: String(header.key).trim(), value: String(header.value).trim() })
|
||||
}
|
||||
|
||||
form.request.headers = headers;
|
||||
emitter('submit', { reference: toRaw(props.reference), item: toRaw(form) });
|
||||
}
|
||||
|
||||
|
||||
const importItem = async () => {
|
||||
let val = import_string.value.trim()
|
||||
if (!val) {
|
||||
toast.error('The import string is required.')
|
||||
return
|
||||
}
|
||||
|
||||
if (false === val.startsWith('{')) {
|
||||
try {
|
||||
val = base64UrlDecode(val)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to decode string. ${e.message}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const item = JSON.parse(val)
|
||||
|
||||
if ('notification' !== item._type) {
|
||||
toast.error(`Invalid import string. Expected type 'notification', got '${item._type}'.`)
|
||||
import_string.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (form.target) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (item.name) {
|
||||
form.name = item.name
|
||||
}
|
||||
|
||||
if (item.url) {
|
||||
form.url = item.url
|
||||
}
|
||||
|
||||
if (item.request) {
|
||||
form.request = item.request
|
||||
}
|
||||
|
||||
if (item.on) {
|
||||
form.on = item.on
|
||||
}
|
||||
|
||||
import_string.value = ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to import task. ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,5 @@
|
|||
<template>
|
||||
<main class="columns mt-2 is-multiline">
|
||||
<div class="column is-12">
|
||||
<h1 class="is-pointer is-unselectable title is-5" @click="importExpanded = !importExpanded">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid" :class="importExpanded ? 'fa-arrow-up' : 'fa-arrow-down'" /></span>
|
||||
<span>Import preset.</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<form id="importForm" @submit.prevent="importPreset()" v-if="importExpanded">
|
||||
<div class="box">
|
||||
<label class="label" for="json_preset">
|
||||
Base64 encoded JSON string.
|
||||
</label>
|
||||
|
||||
<div class="field has-addons">
|
||||
|
||||
<div class="control has-icons-left is-expanded">
|
||||
<input type="text" class="input" id="json_preset" v-model="json_preset" autocomplete="off">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button form="importForm" class="button is-primary" :disabled="!json_preset" type="submit">
|
||||
<span class="icon"><i class="fa-solid fa-add" /></span>
|
||||
<span>Import</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h1 class="is-unselectable is-pointer title is-5" @click="convertExpanded = !convertExpanded">
|
||||
<span class="icon-text">
|
||||
|
|
@ -40,10 +7,8 @@
|
|||
<span>Convert yt-dlp cli options.</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<form autocomplete="off" id="convertOpts" @submit.prevent="convertOptions()" v-if="convertExpanded">
|
||||
<div class="box">
|
||||
|
||||
<label class="label" for="opts">
|
||||
yt-dlp CLI options
|
||||
</label>
|
||||
|
|
@ -74,18 +39,54 @@
|
|||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<form id="presetForm" @submit.prevent="checkInfo()">
|
||||
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-title">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
||||
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-header-icon" v-if="reference">
|
||||
<button type="button" @click="showImport = !showImport">
|
||||
<span class="icon"><i class="fa-solid" :class="{
|
||||
'fa-arrow-down': !showImport,
|
||||
'fa-arrow-up': showImport,
|
||||
}" /></span>
|
||||
<span>{{ showImport ? 'Hide' : 'Show' }} import</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
|
||||
<div class="columns is-multiline is-mobile">
|
||||
|
||||
<div class="column is-12" v-if="showImport || !reference">
|
||||
<label class="label is-inline" for="import_string">
|
||||
Import string
|
||||
</label>
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left is-expanded">
|
||||
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
|
||||
<span class="icon"><i class="fa-solid fa-add" /></span>
|
||||
<span>Import</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>You can use this field to populate the data, using shared string.</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||
|
|
@ -112,8 +113,7 @@
|
|||
<span>The yt-dlp <code>[--format, -f]</code> video format code. see <NuxtLink
|
||||
href="https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#format-selection" target="blank">this
|
||||
url</NuxtLink> for more info.</span>. Note, as this key is required, you can set the value to
|
||||
<code>not_set</code>
|
||||
to use the default yt-dlp format.
|
||||
<code>default</code> to let <code>yt-dlp</code> choose the best format.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -218,24 +218,23 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="presetForm">
|
||||
<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 class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="presetForm">
|
||||
<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>
|
||||
</form>
|
||||
|
|
@ -247,6 +246,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -276,8 +276,8 @@ const toast = useToast()
|
|||
const convertInProgress = ref(false)
|
||||
const form = reactive(JSON.parse(JSON.stringify(props.preset)))
|
||||
const opts = ref('')
|
||||
const json_preset = ref('')
|
||||
const importExpanded = ref(false)
|
||||
const import_string = ref('')
|
||||
const showImport = useStorage('showImport', false);
|
||||
const convertExpanded = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -395,10 +395,10 @@ const convertOptions = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const importPreset = async () => {
|
||||
let val = json_preset.value.trim()
|
||||
const importItem = async () => {
|
||||
let val = import_string.value.trim()
|
||||
if (!val) {
|
||||
toast.error('The preset import string is required.')
|
||||
toast.error('The import string is required.')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -406,13 +406,20 @@ const importPreset = async () => {
|
|||
try {
|
||||
val = base64UrlDecode(val)
|
||||
} catch (e) {
|
||||
toast.error('Invalid base64 string.', e)
|
||||
console.error(e)
|
||||
toast.error(`Failed to decode string. ${e.message}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const preset = JSON.parse(val)
|
||||
const item = JSON.parse(val)
|
||||
|
||||
if ('preset' !== item._type) {
|
||||
toast.error(`Invalid import string. Expected type 'preset', got '${item._type}'.`)
|
||||
import_string.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (form.format || form.args || form.postprocessors) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
|
|
@ -420,35 +427,35 @@ const importPreset = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (preset.name) {
|
||||
form.name = preset.name
|
||||
if (item.name) {
|
||||
form.name = item.name
|
||||
}
|
||||
|
||||
if (preset.format) {
|
||||
form.format = preset.format
|
||||
if (item.format) {
|
||||
form.format = item.format
|
||||
}
|
||||
|
||||
if (preset.args) {
|
||||
form.args = JSON.stringify(preset.args, null, 2)
|
||||
if (item.args) {
|
||||
form.args = JSON.stringify(item.args, null, 2)
|
||||
}
|
||||
|
||||
if (preset.postprocessors) {
|
||||
form.postprocessors = JSON.stringify(preset.postprocessors, null, 2)
|
||||
if (item.postprocessors) {
|
||||
form.postprocessors = JSON.stringify(item.postprocessors, null, 2)
|
||||
}
|
||||
|
||||
if (preset.output_template) {
|
||||
form.template = preset.output_template
|
||||
if (item.output_template) {
|
||||
form.template = item.output_template
|
||||
}
|
||||
|
||||
if (preset.folder) {
|
||||
form.folder = preset.folder
|
||||
if (item.folder) {
|
||||
form.folder = item.folder
|
||||
}
|
||||
|
||||
json_preset.value = ''
|
||||
importExpanded.value = false
|
||||
import_string.value = ''
|
||||
showImport.value = false
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to import preset. ${e.message}`)
|
||||
toast.error(`Failed to string. ${e.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,188 +1,232 @@
|
|||
<template>
|
||||
<main class="columns mt-2">
|
||||
<div class="column">
|
||||
<form id="taskForm" @submit.prevent="checkInfo()" class="box">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-12">
|
||||
<h1 class="title is-6" style="border-bottom: 1px solid #dbdbdb;">
|
||||
<main class="columns mt-2 is-multiline">
|
||||
<div class="column is-12">
|
||||
<form autocomplete="off" id="taskForm" @submit.prevent="checkInfo()">
|
||||
<div class="card">
|
||||
|
||||
<div class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fa-solid" :class="reference ? 'fa-cog' : 'fa-plus'" /></span>
|
||||
<span>{{ reference ? 'Edit' : 'Add' }}</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Task name is used to identify the task in the task list and in logs.</span>
|
||||
</span>
|
||||
<div class="card-header-icon" v-if="reference">
|
||||
<button type="button" @click="showImport = !showImport">
|
||||
<span class="icon"><i class="fa-solid" :class="{
|
||||
'fa-arrow-down': !showImport,
|
||||
'fa-arrow-up': showImport,
|
||||
}" /></span>
|
||||
<span>{{ showImport ? 'Hide' : 'Show' }} import</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="url" v-text="'URL'" />
|
||||
<div class="control has-icons-left">
|
||||
<input type="url" class="input" id="url" v-model="form.url" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The channel or playlist URL.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Download path
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
||||
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Downloads are relative to download path, defaults to root path if not set.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-if="showImport || !reference">
|
||||
<label class="label is-inline" for="import_string">
|
||||
Import string
|
||||
</label>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Preset
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
||||
:disabled="addInProgress || hasFormatInConfig"
|
||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
|
||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left is-expanded">
|
||||
<input type="text" class="input" id="import_string" v-model="import_string" autocomplete="off">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-t" /></span>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-primary" :disabled="!import_string" type="button" @click="importItem">
|
||||
<span class="icon"><i class="fa-solid fa-add" /></span>
|
||||
<span>Import</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
|
||||
config.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="timer">
|
||||
CRON expression timer.
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="timer" placeholder="leave empty to run once every hour."
|
||||
v-model="form.timer" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-clock" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
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 <NuxtLink to="https://crontab.guru/"
|
||||
target="_blank">crontab.guru</NuxtLink>.
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>You can use this field to populate the data, using shared string.</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="output_template">
|
||||
Output template
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="output_template" placeholder="The output template to use"
|
||||
v-model="form.template" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The output template to use, if not set, it will defaults to
|
||||
<code>{{ config.app.output_template }}</code>.
|
||||
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||
target="_blank">visit this url</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="config" v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
||||
JSON yt-dlp config or CLI options. <NuxtLink
|
||||
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
|
||||
@click="convertOptions()">Convert to JSON</NuxtLink>
|
||||
</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
|
||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="name" v-text="'Name'" />
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Task name is used to identify the task in the task list and in logs.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span> Extends current global yt-dlp config with given options. Some fields are ignored like
|
||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with caution
|
||||
some of those options can break yt-dlp or the frontend.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="cookies" v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="url" v-text="'URL'" />
|
||||
<div class="control has-icons-left">
|
||||
<input type="url" class="input" id="url" v-model="form.url" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The channel or playlist URL.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use the <NuxtLink target="_blank"
|
||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
||||
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
|
||||
Cookie format.</span>
|
||||
</span>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Preset
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="preset" class="is-fullwidth" v-model="form.preset"
|
||||
:disabled="addInProgress || hasFormatInConfig"
|
||||
v-tooltip.bottom="hasFormatInConfig ? 'Presets are disabled. Format key is present in the config.' : ''">
|
||||
<option v-for="item in config.presets" :key="item.name" :value="item.name">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Select the preset to use for this URL. The preset will be ignored if format key is present in
|
||||
config.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="timer">
|
||||
CRON expression timer.
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="timer" placeholder="leave empty to run once every hour."
|
||||
v-model="form.timer" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-clock" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>
|
||||
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 <NuxtLink to="https://crontab.guru/"
|
||||
target="_blank">crontab.guru</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="folder">
|
||||
Download path
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
|
||||
v-model="form.folder" :disabled="addInProgress" list="folders">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-folder" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Downloads are relative to download path, defaults to root path if not set.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="output_template">
|
||||
Output template
|
||||
</label>
|
||||
<div class="control has-icons-left">
|
||||
<input type="text" class="input" id="output_template" placeholder="The output template to use"
|
||||
v-model="form.template" :disabled="addInProgress">
|
||||
<span class="icon is-small is-left"><i class="fa-solid fa-file" /></span>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>The output template to use, if not set, it will defaults to
|
||||
<code>{{ config.app.output_template }}</code>.
|
||||
For more information <NuxtLink href="https://github.com/yt-dlp/yt-dlp#output-template"
|
||||
target="_blank">visit this url</NuxtLink>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="config"
|
||||
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
|
||||
JSON yt-dlp config or CLI options. <NuxtLink
|
||||
v-if="form.config && (typeof form.config === 'string') && !form.config.trim().startsWith('{')"
|
||||
@click="convertOptions()">Convert to JSON</NuxtLink>
|
||||
</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" id="config" v-model="form.config" :disabled="addInProgress"
|
||||
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span> Extends current global yt-dlp config with given options. Some fields are ignored like
|
||||
<code>cookiefile</code>, <code>paths</code>, and <code>outtmpl</code> etc. Warning: Use with
|
||||
caution
|
||||
some of those options can break yt-dlp or the frontend.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="showAdvanced">
|
||||
<div class="field">
|
||||
<label class="label is-inline" for="cookies"
|
||||
v-tooltip="'Netscape HTTP Cookie format.'">Cookies</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea is-pre" id="cookies" v-model="form.cookies" :disabled="addInProgress" />
|
||||
</div>
|
||||
<span class="help">
|
||||
<span class="icon"><i class="fa-solid fa-info" /></span>
|
||||
<span>Use the <NuxtLink target="_blank"
|
||||
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
|
||||
Recommended addon</NuxtLink> by yt-dlp to export cookies. The cookies MUST be in Netscape HTTP
|
||||
Cookie format.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<p class="control">
|
||||
<button class="button is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="taskForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-danger" @click="emitter('cancel')" :disabled="addInProgress" type="button">
|
||||
<span class="icon"><i class="fa-solid fa-times" /></span>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-info" type="button" @click="showAdvanced = !showAdvanced">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Opts</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<p class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" :disabled="addInProgress" type="submit"
|
||||
:class="{ 'is-loading': addInProgress }" form="taskForm">
|
||||
<span class="icon"><i class="fa-solid fa-save" /></span>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</p>
|
||||
<p 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>
|
||||
</p>
|
||||
<p class="card-footer-item">
|
||||
<button class="button is-fullwidth is-info" type="button" @click="showAdvanced = !showAdvanced">
|
||||
<span class="icon"><i class="fa-solid fa-cog" /></span>
|
||||
<span>Advanced</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -195,14 +239,15 @@
|
|||
|
||||
<script setup>
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { parseExpression } from 'cron-parser'
|
||||
import { request } from '~/utils/index'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
|
||||
const emitter = defineEmits(['cancel', 'submit']);
|
||||
const toast = useToast();
|
||||
const config = useConfigStore();
|
||||
const convertInProgress = ref(false);
|
||||
const showAdvanced = useStorage('task.showAdvanced', false);
|
||||
const showImport = useStorage('showImport', false);
|
||||
const import_string = ref('');
|
||||
|
||||
const props = defineProps({
|
||||
reference: {
|
||||
|
|
@ -244,9 +289,10 @@ const checkInfo = async () => {
|
|||
|
||||
if (form.timer) {
|
||||
try {
|
||||
parseExpression(form.timer);
|
||||
CronExpressionParser.parse(form.timer);
|
||||
} catch (e) {
|
||||
toast.error('Invalid CRON expression.');
|
||||
console.log(e)
|
||||
toast.error(`Invalid CRON expression. ${e.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -311,4 +357,71 @@ const hasFormatInConfig = computed(() => {
|
|||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const importItem = async () => {
|
||||
let val = import_string.value.trim()
|
||||
if (!val) {
|
||||
toast.error('The import string is required.')
|
||||
return
|
||||
}
|
||||
|
||||
if (false === val.startsWith('{')) {
|
||||
try {
|
||||
val = base64UrlDecode(val)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to decode string. ${e.message}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const item = JSON.parse(val)
|
||||
|
||||
if ('task' !== item._type) {
|
||||
toast.error(`Invalid import string. Expected type 'task', got '${item._type}'.`)
|
||||
import_string.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (form.config || form.url || form.timer) {
|
||||
if (false === confirm('This will overwrite the current form fields. Are you sure?')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (item.name) {
|
||||
form.name = item.name
|
||||
}
|
||||
|
||||
if (item.url) {
|
||||
form.url = item.url
|
||||
}
|
||||
|
||||
if (item.preset) {
|
||||
form.preset = item.preset
|
||||
}
|
||||
|
||||
if (item.timer) {
|
||||
form.timer = item.timer
|
||||
}
|
||||
|
||||
if (item.folder) {
|
||||
form.folder = item.folder
|
||||
}
|
||||
|
||||
if (item.template) {
|
||||
form.template = item.template
|
||||
}
|
||||
|
||||
if (item.config) {
|
||||
form.config = JSON.stringify(item.config, null, 2)
|
||||
}
|
||||
|
||||
import_string.value = ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`Failed to import string. ${e.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@
|
|||
},
|
||||
"web-types": "./web-types.json",
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@sentry/nuxt": "^9.2.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/nuxt": "^10.9.0",
|
||||
"@pinia/nuxt": "^0.10.1",
|
||||
"@sentry/nuxt": "^9.5.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cron-parser": "^5.0.4",
|
||||
"cronstrue": "^2.49.0",
|
||||
"floating-vue": "^5.2.2",
|
||||
"hls.js": "^1.4.12",
|
||||
"moment": "^2.30.1",
|
||||
"nuxt": "^3.16",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia": "^3.0.1",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.4",
|
||||
"vue-router": "^4.3.0",
|
||||
|
|
|
|||
|
|
@ -66,9 +66,8 @@ div.is-centered {
|
|||
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
|
||||
</div>
|
||||
<div class="card-header-icon">
|
||||
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'"
|
||||
@click.prevent="copyText(item.request.url)">
|
||||
<span class="icon"><i class="fa-solid fa-copy" /></span>
|
||||
<a class="has-text-primary" v-tooltip="'Export target.'" @click.prevent="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
<button @click="item.raw = !item.raw">
|
||||
<span class="icon"><i class="fa-solid"
|
||||
|
|
@ -84,7 +83,7 @@ div.is-centered {
|
|||
</p>
|
||||
<p v-if="item.request?.headers && item.request.headers.length > 0">
|
||||
<span class="icon"><i class="fa-solid fa-heading" /></span>
|
||||
<span>{{ item.request.headers.map(h => h.key).join(', ') }}</span>
|
||||
<span>{{item.request.headers.map(h => h.key).join(', ')}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,6 +115,19 @@ div.is-centered {
|
|||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="notifications && notifications.length > 0 && !toggleForm">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
When you export notification target, We remove <code>Authorization</code> header key by default,
|
||||
However this might not be enough to remove credentials from the exported data. it's your responsibility
|
||||
to ensure that the exported data does not contain any sensitive information for sharing.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -301,4 +313,23 @@ const sendTest = async () => {
|
|||
|
||||
onMounted(async () => socket.isConnected ? await reloadContent(true) : '')
|
||||
|
||||
const exportItem = async item => {
|
||||
let data = JSON.parse(JSON.stringify(item))
|
||||
const keys = ['id', 'raw']
|
||||
keys.forEach(k => {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
delete data[k]
|
||||
}
|
||||
})
|
||||
|
||||
// -- Remove authorization headers
|
||||
if (data.request?.headers && data.request.headers.length > 0) {
|
||||
data.request.headers = data.request.headers.filter(h => h.key.toLowerCase() !== 'authorization')
|
||||
}
|
||||
|
||||
data['_type'] = 'notification'
|
||||
data['_version'] = '1.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ div.is-centered {
|
|||
<header class="card-header">
|
||||
<div class="card-header-title is-text-overflow is-block" v-text="item.name" />
|
||||
<div class="card-header-icon">
|
||||
<a class="has-text-primary" v-tooltip="'Export preset.'" @click.prevent="copyItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-copy" /></span>
|
||||
<a class="has-text-primary" v-tooltip="'Export preset.'" @click.prevent="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
<button @click="item.raw = !item.raw">
|
||||
<span class="icon"><i class="fa-solid" :class="{
|
||||
|
|
@ -116,12 +116,11 @@ div.is-centered {
|
|||
v-if="!presets || presets.length < 1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<div class="column is-12" v-if="presets && presets.length > 0 && !toggleForm">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
When you export the preset, it doesn't include <code>Cookies</code> field for security reasons. The exported
|
||||
string is URL safe base64 encoded JSON string.
|
||||
When you export preset, it doesn't include <code>Cookies</code> field for security reasons.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
|
|
@ -285,10 +284,10 @@ const editItem = (item) => {
|
|||
|
||||
onMounted(async () => (socket.isConnected ? await reloadContent(true) : ""));
|
||||
|
||||
const copyItem = (item) => {
|
||||
const exportItem = item => {
|
||||
let data = JSON.parse(JSON.stringify(item));
|
||||
const keys = ["id", "default", "raw", "cookies"];
|
||||
keys.forEach((key) => {
|
||||
keys.forEach(key => {
|
||||
if (key in data) {
|
||||
delete data[key];
|
||||
}
|
||||
|
|
@ -311,6 +310,9 @@ const copyItem = (item) => {
|
|||
userData[key] = data[key];
|
||||
}
|
||||
|
||||
userData['_type'] = 'preset';
|
||||
userData['_version'] = '1.0'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(userData)));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ div.is-centered {
|
|||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">
|
||||
The task runner is simply queuing given urls at the specified time. It's basically doing what you are doing
|
||||
when you click the add button on the WebGUI, this just fancy way to automate that.
|
||||
The task runner is simple queue system that allows you to schedule downloads. The tasks are run at the
|
||||
scheduled time.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -59,9 +59,8 @@ div.is-centered {
|
|||
<NuxtLink target="_blank" :href="item.url">{{ item.name }}</NuxtLink>
|
||||
</div>
|
||||
<div class="card-header-icon">
|
||||
<a :href="item.url" class="has-text-primary" v-tooltip="'Copy url.'"
|
||||
@click.prevent="copyText(item.url)">
|
||||
<span class="icon"><i class="fa-solid fa-copy" /></span>
|
||||
<a class="has-text-primary" v-tooltip="'Export task.'" @click.prevent="exportItem(item)">
|
||||
<span class="icon"><i class="fa-solid fa-file-export" /></span>
|
||||
</a>
|
||||
<button @click="item.raw = !item.raw">
|
||||
<span class="icon"><i class="fa-solid"
|
||||
|
|
@ -121,12 +120,23 @@ div.is-centered {
|
|||
icon="fas fa-exclamation-circle" v-if="!tasks || tasks.length < 1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12" v-if="tasks && tasks.length > 0 && !toggleForm">
|
||||
<Message message_class="has-background-info-90 has-text-dark" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
When you export task, the preset settings is automatically merged into the task and the preset set to
|
||||
<code>default</code> to be more portable. The exporter doesn't include <code>Cookies</code> field for
|
||||
security reasons.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import moment from 'moment'
|
||||
import { parseExpression } from 'cron-parser'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
import { request } from '~/utils/index'
|
||||
|
||||
const toast = useToast()
|
||||
|
|
@ -293,7 +303,7 @@ onMounted(async () => {
|
|||
|
||||
const tryParse = expression => {
|
||||
try {
|
||||
return moment(parseExpression(expression).next().toISOString()).fromNow()
|
||||
return moment(CronExpressionParser.parse(expression).next().toISOString()).fromNow()
|
||||
} catch (e) {
|
||||
return "Invalid"
|
||||
}
|
||||
|
|
@ -347,4 +357,72 @@ const statusHandler = async stream => {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
const exportItem = async item => {
|
||||
let preset = config.presets.find(p => p.name === item.preset)
|
||||
if (!preset) {
|
||||
toast.error('Preset not found.')
|
||||
return
|
||||
}
|
||||
|
||||
const info = JSON.parse(JSON.stringify(item))
|
||||
preset = JSON.parse(JSON.stringify(preset))
|
||||
|
||||
let data = {
|
||||
name: info.name,
|
||||
url: info.url,
|
||||
preset: 'default',
|
||||
timer: info.timer,
|
||||
folder: info.folder,
|
||||
template: info.template,
|
||||
config: info.config,
|
||||
}
|
||||
|
||||
// -- merge preset options with task args.
|
||||
let args = {}
|
||||
|
||||
if (preset.args && Object.keys(preset.args).length > 0) {
|
||||
for (const key of Object.keys(preset.args)) {
|
||||
args[key] = preset.args[key]
|
||||
}
|
||||
}
|
||||
const defaults = ['default', 'not_set']
|
||||
if (preset.format && !defaults.includes(preset.format)) {
|
||||
args.format = preset.format
|
||||
}
|
||||
|
||||
if (preset.postprocessors && preset.postprocessors.length > 0) {
|
||||
args.postprocessors = preset.postprocessors
|
||||
}
|
||||
|
||||
if (preset.folder && !info.folder) {
|
||||
data.folder = preset.folder
|
||||
}
|
||||
|
||||
if (preset.template && !info.template) {
|
||||
data.template = preset.template
|
||||
}
|
||||
|
||||
if (!data.config || Object.keys(data.config).length < 1) {
|
||||
data.config = {}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(args)) {
|
||||
if (key in data.config && Array.isArray(args[key]) && Array.isArray(data.config[key])) {
|
||||
data.config[key] = data.config[key].concat(args[key])
|
||||
continue
|
||||
}
|
||||
|
||||
if (data?.config[key]) {
|
||||
continue
|
||||
}
|
||||
|
||||
data.config[key] = args[key]
|
||||
}
|
||||
|
||||
data['_type'] = 'task'
|
||||
data['_version'] = '1.1'
|
||||
|
||||
return copyText(base64UrlEncode(JSON.stringify(data)));
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
1143
ui/yarn.lock
1143
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue